@trazum/cli 1.43.0 → 1.44.0
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 +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +32 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +35 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +16 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -1
- package/dist/serve.d.ts +65 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +115 -0
- package/dist/serve.js.map +1 -0
- package/package.json +2 -2
- package/src/i18n/en.ts +37 -0
- package/src/i18n/es.ts +40 -0
- package/src/i18n/types.ts +17 -0
- package/src/index.ts +79 -0
- package/src/serve.ts +149 -0
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The endpoint that answers before the call is sent.
|
|
3
|
+
*
|
|
4
|
+
* **Loopback only, and the address is not a flag.** A cost oracle listening on
|
|
5
|
+
* a network interface is an attack surface with a very small upside: it holds
|
|
6
|
+
* a company's spend, its model mix and its budgets, and it answers anybody who
|
|
7
|
+
* asks. `checkedEndpoint` has guarded Trazum's *outbound* requests since 1.14
|
|
8
|
+
* on the principle that a caller selects an endpoint rather than naming one;
|
|
9
|
+
* this is the inbound counterpart, and it is enforced the same way — by there
|
|
10
|
+
* being no way to say otherwise. `127.0.0.1` is compiled in. A Unix socket is
|
|
11
|
+
* offered for callers that would rather not use a port at all.
|
|
12
|
+
*
|
|
13
|
+
* **No auth, on purpose.** Anything reachable only from the machine it runs on
|
|
14
|
+
* is already behind the operating system's own boundary, and a token checked
|
|
15
|
+
* over loopback is theatre: whoever can reach the socket can read the token
|
|
16
|
+
* out of the process that holds it. The honest posture is a surface small
|
|
17
|
+
* enough not to need one.
|
|
18
|
+
*
|
|
19
|
+
* **It degrades rather than failing.** With no store and no budget the
|
|
20
|
+
* endpoint still prices the call from the bundled catalogue and says the
|
|
21
|
+
* budget half is unknown. Offline is a mode, not an error, and an oracle that
|
|
22
|
+
* refuses to speak when half its inputs are missing is an oracle nobody wires
|
|
23
|
+
* into a hot path.
|
|
24
|
+
*/
|
|
25
|
+
import type { Server } from 'node:http';
|
|
26
|
+
import type { PricingCatalogue } from '@trazum/core';
|
|
27
|
+
/** Compiled in. See the module note: this is the inbound SSRF posture. */
|
|
28
|
+
export declare const BIND_HOST = "127.0.0.1";
|
|
29
|
+
export declare const DEFAULT_PORT = 7317;
|
|
30
|
+
/**
|
|
31
|
+
* Bodies larger than this are refused unread.
|
|
32
|
+
*
|
|
33
|
+
* A prompt is text and text is unbounded; a hot-path oracle that will buffer
|
|
34
|
+
* whatever it is handed is a memory exhaustion away from taking the caller
|
|
35
|
+
* down with it — and the caller was asking how to spend *less*.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_BODY_BYTES = 1000000;
|
|
38
|
+
export interface ServeContext {
|
|
39
|
+
catalogue: PricingCatalogue;
|
|
40
|
+
/**
|
|
41
|
+
* Measured spend and the budget it is judged against, read once at start
|
|
42
|
+
* and refreshed by the caller.
|
|
43
|
+
*
|
|
44
|
+
* Read once because the whole promise here is single-digit milliseconds,
|
|
45
|
+
* and a file read in the request path cannot make that promise. The staleness
|
|
46
|
+
* is a real cost, so the answer carries the window its measurement covers
|
|
47
|
+
* rather than implying it is current to the second.
|
|
48
|
+
*/
|
|
49
|
+
position: () => {
|
|
50
|
+
consumedUsd?: number;
|
|
51
|
+
limitUsd?: number;
|
|
52
|
+
window?: {
|
|
53
|
+
fromMs: number;
|
|
54
|
+
toMs: number;
|
|
55
|
+
} | null;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export declare function buildServer(context: ServeContext): Server;
|
|
59
|
+
export interface ListenTarget {
|
|
60
|
+
/** A Unix socket path, when the caller would rather not use a port. */
|
|
61
|
+
socket?: string;
|
|
62
|
+
port?: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function listen(server: Server, target: ListenTarget): Promise<string>;
|
|
65
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAmB,MAAM,EAAkB,MAAM,WAAW,CAAC;AAEzE,OAAO,KAAK,EAAc,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEjE,0EAA0E;AAC1E,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,UAAY,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;;;;;;OAQG;IACH,QAAQ,EAAE,MAAM;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CAC/G;AAyBD,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CA8CzD;AAED,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAa5E"}
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The endpoint that answers before the call is sent.
|
|
3
|
+
*
|
|
4
|
+
* **Loopback only, and the address is not a flag.** A cost oracle listening on
|
|
5
|
+
* a network interface is an attack surface with a very small upside: it holds
|
|
6
|
+
* a company's spend, its model mix and its budgets, and it answers anybody who
|
|
7
|
+
* asks. `checkedEndpoint` has guarded Trazum's *outbound* requests since 1.14
|
|
8
|
+
* on the principle that a caller selects an endpoint rather than naming one;
|
|
9
|
+
* this is the inbound counterpart, and it is enforced the same way — by there
|
|
10
|
+
* being no way to say otherwise. `127.0.0.1` is compiled in. A Unix socket is
|
|
11
|
+
* offered for callers that would rather not use a port at all.
|
|
12
|
+
*
|
|
13
|
+
* **No auth, on purpose.** Anything reachable only from the machine it runs on
|
|
14
|
+
* is already behind the operating system's own boundary, and a token checked
|
|
15
|
+
* over loopback is theatre: whoever can reach the socket can read the token
|
|
16
|
+
* out of the process that holds it. The honest posture is a surface small
|
|
17
|
+
* enough not to need one.
|
|
18
|
+
*
|
|
19
|
+
* **It degrades rather than failing.** With no store and no budget the
|
|
20
|
+
* endpoint still prices the call from the bundled catalogue and says the
|
|
21
|
+
* budget half is unknown. Offline is a mode, not an error, and an oracle that
|
|
22
|
+
* refuses to speak when half its inputs are missing is an oracle nobody wires
|
|
23
|
+
* into a hot path.
|
|
24
|
+
*/
|
|
25
|
+
import { createServer } from 'node:http';
|
|
26
|
+
import { answerCost } from '@trazum/core';
|
|
27
|
+
/** Compiled in. See the module note: this is the inbound SSRF posture. */
|
|
28
|
+
export const BIND_HOST = '127.0.0.1';
|
|
29
|
+
export const DEFAULT_PORT = 7317;
|
|
30
|
+
/**
|
|
31
|
+
* Bodies larger than this are refused unread.
|
|
32
|
+
*
|
|
33
|
+
* A prompt is text and text is unbounded; a hot-path oracle that will buffer
|
|
34
|
+
* whatever it is handed is a memory exhaustion away from taking the caller
|
|
35
|
+
* down with it — and the caller was asking how to spend *less*.
|
|
36
|
+
*/
|
|
37
|
+
export const MAX_BODY_BYTES = 1_000_000;
|
|
38
|
+
async function readBody(request) {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let size = 0;
|
|
41
|
+
for await (const chunk of request) {
|
|
42
|
+
size += chunk.length;
|
|
43
|
+
if (size > MAX_BODY_BYTES)
|
|
44
|
+
throw new Error('body too large');
|
|
45
|
+
chunks.push(chunk);
|
|
46
|
+
}
|
|
47
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
48
|
+
}
|
|
49
|
+
const send = (response, status, body) => {
|
|
50
|
+
const text = JSON.stringify(body);
|
|
51
|
+
response.writeHead(status, {
|
|
52
|
+
'content-type': 'application/json',
|
|
53
|
+
'content-length': Buffer.byteLength(text),
|
|
54
|
+
// Nothing here is for a browser to read across origins, and saying so
|
|
55
|
+
// costs one header.
|
|
56
|
+
'cache-control': 'no-store',
|
|
57
|
+
});
|
|
58
|
+
response.end(text);
|
|
59
|
+
};
|
|
60
|
+
export function buildServer(context) {
|
|
61
|
+
return createServer((request, response) => {
|
|
62
|
+
void (async () => {
|
|
63
|
+
const url = new URL(request.url ?? '/', `http://${BIND_HOST}`);
|
|
64
|
+
if (request.method === 'GET' && url.pathname === '/health') {
|
|
65
|
+
send(response, 200, { ok: true, schemaVersion: 1 });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (request.method !== 'POST' || url.pathname !== '/cost') {
|
|
69
|
+
send(response, 404, {
|
|
70
|
+
error: 'not-found',
|
|
71
|
+
detail: 'POST /cost, or GET /health.',
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let payload;
|
|
76
|
+
try {
|
|
77
|
+
const raw = await readBody(request);
|
|
78
|
+
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
send(response, 400, {
|
|
82
|
+
error: 'bad-request',
|
|
83
|
+
detail: error instanceof Error && error.message === 'body too large'
|
|
84
|
+
? `A request body may be at most ${MAX_BODY_BYTES} bytes.`
|
|
85
|
+
: 'The body must be JSON.',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const position = context.position();
|
|
90
|
+
const answer = answerCost({
|
|
91
|
+
model: typeof payload.model === 'string' ? payload.model : undefined,
|
|
92
|
+
inputTokens: typeof payload.inputTokens === 'number' ? payload.inputTokens : undefined,
|
|
93
|
+
outputTokens: typeof payload.outputTokens === 'number' ? payload.outputTokens : undefined,
|
|
94
|
+
basis: payload.basis === 'heuristic' ? 'heuristic' : 'token-count',
|
|
95
|
+
...position,
|
|
96
|
+
}, { catalogue: context.catalogue });
|
|
97
|
+
send(response, 200, answer);
|
|
98
|
+
})();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function listen(server, target) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
server.once('error', reject);
|
|
104
|
+
if (target.socket !== undefined) {
|
|
105
|
+
server.listen(target.socket, () => resolve(target.socket));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// The host is not a parameter. See the module note.
|
|
109
|
+
server.listen(target.port ?? DEFAULT_PORT, BIND_HOST, () => {
|
|
110
|
+
const address = server.address();
|
|
111
|
+
resolve(typeof address === 'object' && address !== null ? `${BIND_HOST}:${address.port}` : String(address));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build && npm test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@trazum/core": "1.
|
|
40
|
+
"@trazum/core": "1.44.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -45,6 +45,7 @@ ${bold('USAGE')}
|
|
|
45
45
|
trazum connect <anthropic|openai> [options]
|
|
46
46
|
trazum store [--prune] [options]
|
|
47
47
|
trazum watch [--once | --interval 15m] [options]
|
|
48
|
+
trazum serve [--port <n> | --socket <path>]
|
|
48
49
|
trazum diff <before> <after> [options]
|
|
49
50
|
trazum diff --all <dir> <dir> [options]
|
|
50
51
|
trazum rank <dir> [options]
|
|
@@ -322,6 +323,29 @@ ${bold('OPTIONS FOR plan')}
|
|
|
322
323
|
a plan that hides its assumptions is advice pretending to be arithmetic.
|
|
323
324
|
Projected savings and money already spent are separate totals throughout.
|
|
324
325
|
|
|
326
|
+
${bold('OPTIONS FOR serve')}
|
|
327
|
+
--port <n> Port on 127.0.0.1. Default: 7317.
|
|
328
|
+
--socket <path> Listen on a Unix socket instead of a port.
|
|
329
|
+
|
|
330
|
+
Answers the two questions that matter at call time — what will this cost,
|
|
331
|
+
and is there budget left — in single-digit milliseconds, so an agent or a
|
|
332
|
+
wrapper can ask before it spends rather than reading a report afterwards.
|
|
333
|
+
|
|
334
|
+
POST /cost with {"model": "...", "inputTokens": n, "outputTokens": n};
|
|
335
|
+
GET /health says it is up. Every answer keeps the measured half and the
|
|
336
|
+
estimated half apart: the budget consumed comes from the provider's billed
|
|
337
|
+
counts, the cost of the call being asked about is an estimate of something
|
|
338
|
+
that has not happened, and the verdict says which of the two it rests on.
|
|
339
|
+
|
|
340
|
+
It listens on 127.0.0.1 and nowhere else, and there is no flag to change
|
|
341
|
+
that: a cost oracle on a network interface holds a company's spend, its
|
|
342
|
+
model mix and its budgets, and answers whoever asks. There is no auth for
|
|
343
|
+
the same reason there is no --host — a token checked over loopback is
|
|
344
|
+
theatre, and the honest posture is a surface small enough not to need one.
|
|
345
|
+
|
|
346
|
+
With no store and no budget it still prices the call and says the budget
|
|
347
|
+
half is unknown. Offline is a mode, not a failure.
|
|
348
|
+
|
|
325
349
|
${bold('OPTIONS FOR watch')}
|
|
326
350
|
--once One cycle: measure, keep, evaluate, emit,
|
|
327
351
|
remember. What a cron entry runs. The default.
|
|
@@ -1536,6 +1560,19 @@ ${bold('EXAMPLES')}
|
|
|
1536
1560
|
`Plan written to ${path}, dated. Keep it: a prediction nobody wrote down is a prediction nobody can be held to.`,
|
|
1537
1561
|
},
|
|
1538
1562
|
|
|
1563
|
+
serve: {
|
|
1564
|
+
listening: (where) => `Answering on ${where}`,
|
|
1565
|
+
loopbackOnly: () =>
|
|
1566
|
+
'Loopback only, and there is no flag to change that: this holds your spend, your model mix and your budgets, and would answer whoever asked. There is no auth for the same reason — a token checked over loopback is theatre.',
|
|
1567
|
+
measuredFrom: (usd) =>
|
|
1568
|
+
`Budget answers are measured against ${usd} from the store, read once at start. Every answer carries the window that figure covers rather than implying it is current to the second — restart to refresh it.`,
|
|
1569
|
+
nothingMeasured: (dir) =>
|
|
1570
|
+
`Nothing is measured yet (the store at ${dir} is empty), so the budget half of every answer will say so. The cost half still answers from the catalogue: offline is a mode, not a failure.`,
|
|
1571
|
+
noBudget: () =>
|
|
1572
|
+
'No spend.maxUsd is configured, so "is there budget left" has no subject and every answer says so rather than guessing one.',
|
|
1573
|
+
badPort: (value) => `"${value}" is not a port. Give a whole number from 0 to 65535, or use --socket.`,
|
|
1574
|
+
},
|
|
1575
|
+
|
|
1539
1576
|
watch: {
|
|
1540
1577
|
noThresholds: () =>
|
|
1541
1578
|
'Watching needs something to watch for. Set spend.maxUsd, spend.maxDayUsd or spend.maxCacheLossUsd in trazum.config.json — a watcher with no threshold is a green light nobody earned.',
|
package/src/i18n/es.ts
CHANGED
|
@@ -32,6 +32,7 @@ ${bold('USO')}
|
|
|
32
32
|
trazum connect <anthropic|openai> [opciones]
|
|
33
33
|
trazum store [--prune] [opciones]
|
|
34
34
|
trazum watch [--once | --interval 15m] [opciones]
|
|
35
|
+
trazum serve [--port <n> | --socket <ruta>]
|
|
35
36
|
trazum diff <antes> <después> [opciones]
|
|
36
37
|
trazum diff --all <dir> <dir> [opciones]
|
|
37
38
|
trazum rank <dir> [opciones]
|
|
@@ -329,6 +330,32 @@ ${bold('OPCIONES DE plan')}
|
|
|
329
330
|
consejo haciéndose pasar por aritmética. El ahorro proyectado y el dinero ya
|
|
330
331
|
gastado son totales separados en todas partes.
|
|
331
332
|
|
|
333
|
+
${bold('OPCIONES DE serve')}
|
|
334
|
+
--port <n> Puerto en 127.0.0.1. Por defecto: 7317.
|
|
335
|
+
--socket <ruta> Escucha en un socket Unix en vez de un puerto.
|
|
336
|
+
|
|
337
|
+
Responde las dos preguntas que importan en el momento de la llamada — cuánto
|
|
338
|
+
va a costar esto y si queda presupuesto — en milisegundos de un solo dígito,
|
|
339
|
+
para que un agente o un envoltorio pueda preguntar antes de gastar en vez de
|
|
340
|
+
leer un informe después.
|
|
341
|
+
|
|
342
|
+
POST /cost con {"model": "...", "inputTokens": n, "outputTokens": n};
|
|
343
|
+
GET /health dice que está en pie. Cada respuesta mantiene separada la mitad
|
|
344
|
+
medida de la estimada: el presupuesto consumido viene de los recuentos que
|
|
345
|
+
facturó el proveedor, el coste de la llamada por la que preguntas es una
|
|
346
|
+
estimación de algo que no ha pasado, y el veredicto dice en cuál de las dos
|
|
347
|
+
se apoya.
|
|
348
|
+
|
|
349
|
+
Escucha en 127.0.0.1 y en ningún otro sitio, y no hay flag para cambiarlo:
|
|
350
|
+
un oráculo de costes en una interfaz de red guarda el gasto de una empresa,
|
|
351
|
+
su mezcla de modelos y sus presupuestos, y le responde a quien pregunte. No
|
|
352
|
+
hay autenticación por la misma razón por la que no hay --host — un token
|
|
353
|
+
comprobado sobre loopback es teatro, y la postura honesta es una superficie
|
|
354
|
+
lo bastante pequeña como para no necesitarlo.
|
|
355
|
+
|
|
356
|
+
Sin almacén y sin presupuesto sigue tasando la llamada y dice que la mitad
|
|
357
|
+
del presupuesto es desconocida. Sin conexión es un modo, no un fallo.
|
|
358
|
+
|
|
332
359
|
${bold('OPCIONES DE watch')}
|
|
333
360
|
--once Una vuelta: medir, guardar, evaluar, emitir,
|
|
334
361
|
recordar. Lo que ejecuta una entrada de cron. Es
|
|
@@ -1563,6 +1590,19 @@ ${bold('EJEMPLOS')}
|
|
|
1563
1590
|
`Plan escrito en ${path}, con fecha. Guárdalo: una predicción que nadie apuntó es una predicción que no se le puede exigir a nadie.`,
|
|
1564
1591
|
},
|
|
1565
1592
|
|
|
1593
|
+
serve: {
|
|
1594
|
+
listening: (where) => `Respondiendo en ${where}`,
|
|
1595
|
+
loopbackOnly: () =>
|
|
1596
|
+
'Solo loopback, y no hay flag para cambiarlo: esto guarda tu gasto, tu mezcla de modelos y tus presupuestos, y le respondería a quien preguntara. No hay autenticación por la misma razón — un token comprobado sobre loopback es teatro.',
|
|
1597
|
+
measuredFrom: (usd) =>
|
|
1598
|
+
`Las respuestas de presupuesto se miden contra ${usd} del almacén, leídos una vez al arrancar. Cada respuesta lleva el período que cubre esa cifra en vez de insinuar que está al segundo — reinicia para refrescarla.`,
|
|
1599
|
+
nothingMeasured: (dir) =>
|
|
1600
|
+
`Todavía no hay nada medido (el almacén de ${dir} está vacío), así que la mitad de presupuesto de cada respuesta lo dirá. La mitad del coste sigue respondiendo desde el catálogo: sin conexión es un modo, no un fallo.`,
|
|
1601
|
+
noBudget: () =>
|
|
1602
|
+
'No hay spend.maxUsd configurado, así que "queda presupuesto" no tiene sujeto y cada respuesta lo dice en vez de inventarse uno.',
|
|
1603
|
+
badPort: (value) => `"${value}" no es un puerto. Da un número entero de 0 a 65535, o usa --socket.`,
|
|
1604
|
+
},
|
|
1605
|
+
|
|
1566
1606
|
watch: {
|
|
1567
1607
|
noThresholds: () =>
|
|
1568
1608
|
'Vigilar necesita algo que vigilar. Define spend.maxUsd, spend.maxDayUsd o spend.maxCacheLossUsd en trazum.config.json — un vigilante sin umbral es una luz verde que nadie se ha ganado.',
|
package/src/i18n/types.ts
CHANGED
|
@@ -1072,6 +1072,23 @@ export interface CliMessages {
|
|
|
1072
1072
|
wrote(path: string): string;
|
|
1073
1073
|
};
|
|
1074
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
* `trazum serve` — the answer given before the call is sent.
|
|
1077
|
+
*
|
|
1078
|
+
* The copy states the two things a reader must not have to infer: that this
|
|
1079
|
+
* listens on loopback and nowhere else, and that the measured half is read
|
|
1080
|
+
* once rather than being current to the second.
|
|
1081
|
+
*/
|
|
1082
|
+
serve: {
|
|
1083
|
+
listening(where: string): string;
|
|
1084
|
+
/** Why there is no --host, and why that is not an omission. */
|
|
1085
|
+
loopbackOnly(): string;
|
|
1086
|
+
measuredFrom(usd: string): string;
|
|
1087
|
+
nothingMeasured(dir: string): string;
|
|
1088
|
+
noBudget(): string;
|
|
1089
|
+
badPort(value: string): string;
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1075
1092
|
/**
|
|
1076
1093
|
* `trazum watch` — the gates, evaluated as the money moves.
|
|
1077
1094
|
*
|
package/src/index.ts
CHANGED
|
@@ -148,6 +148,7 @@ import {
|
|
|
148
148
|
import type { Revision } from './git.js';
|
|
149
149
|
import { fetchProviderUsage } from './connect.js';
|
|
150
150
|
import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
|
|
151
|
+
import { DEFAULT_PORT, buildServer, listen } from './serve.js';
|
|
151
152
|
import {
|
|
152
153
|
WATCH_STATE_VERSION,
|
|
153
154
|
checkWebhook,
|
|
@@ -207,6 +208,8 @@ const VALUE_FLAGS = new Set([
|
|
|
207
208
|
'keep',
|
|
208
209
|
'interval',
|
|
209
210
|
'webhook',
|
|
211
|
+
'port',
|
|
212
|
+
'socket',
|
|
210
213
|
// `route` takes a path here, and the flag is deliberately not `--prompt`:
|
|
211
214
|
// everywhere else in this tool `--prompt` names a marked prompt *inside* a
|
|
212
215
|
// source file, and reusing it for a path would be a trap laid for the reader.
|
|
@@ -513,6 +516,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
513
516
|
connect: ['since', 'until', 'payload', 'store', 'json', 'out', 'markdown-out', 'pricing', 'pricing-live', 'dry-run'],
|
|
514
517
|
store: ['prune', 'keep', 'json', 'pricing', 'pricing-live', 'dry-run'],
|
|
515
518
|
watch: ['once', 'interval', 'since', 'payload', 'webhook', 'json', 'pricing', 'pricing-live'],
|
|
519
|
+
serve: ['port', 'socket', 'pricing', 'pricing-live'],
|
|
516
520
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
517
521
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
518
522
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -2294,6 +2298,78 @@ function parseWhen(
|
|
|
2294
2298
|
throw new Error(t.profile.badWhen(flag, value));
|
|
2295
2299
|
}
|
|
2296
2300
|
|
|
2301
|
+
/**
|
|
2302
|
+
* `trazum serve` — the answer in milliseconds.
|
|
2303
|
+
*
|
|
2304
|
+
* The measured position is read once at start rather than per request: the
|
|
2305
|
+
* whole promise is a single-digit-millisecond answer, and a file read in the
|
|
2306
|
+
* hot path cannot make it. That staleness is real, so every answer carries the
|
|
2307
|
+
* window its measurement covers instead of implying it is current to the
|
|
2308
|
+
* second.
|
|
2309
|
+
*/
|
|
2310
|
+
async function commandServe(
|
|
2311
|
+
args: Args,
|
|
2312
|
+
config: TrazumConfig,
|
|
2313
|
+
pricing: PricingCatalogue,
|
|
2314
|
+
t: CliMessages,
|
|
2315
|
+
): Promise<void> {
|
|
2316
|
+
const root = process.cwd();
|
|
2317
|
+
const limitUsd = config.spend?.maxUsd;
|
|
2318
|
+
|
|
2319
|
+
const { resolved } = await readStore(root);
|
|
2320
|
+
const measured = resolved.records.length > 0;
|
|
2321
|
+
/**
|
|
2322
|
+
* The window the measurement covers, carried into every answer.
|
|
2323
|
+
*
|
|
2324
|
+
* The position is read once at start, so a caller has to be able to see how
|
|
2325
|
+
* old it is. A null window here would let a figure from last month read as
|
|
2326
|
+
* current, which is the staleness this endpoint is otherwise honest about.
|
|
2327
|
+
*/
|
|
2328
|
+
const window = measured
|
|
2329
|
+
? {
|
|
2330
|
+
fromMs: Math.min(...resolved.records.map((record) => record.fromMs)),
|
|
2331
|
+
toMs: Math.max(...resolved.records.map((record) => record.toMs)),
|
|
2332
|
+
}
|
|
2333
|
+
: null;
|
|
2334
|
+
const report = bucketedProfile(
|
|
2335
|
+
{
|
|
2336
|
+
provider: 'store',
|
|
2337
|
+
granularity: 'bucketed',
|
|
2338
|
+
buckets: bucketsFromRecords(resolved.records),
|
|
2339
|
+
window,
|
|
2340
|
+
gaps: [],
|
|
2341
|
+
unavailable: [],
|
|
2342
|
+
},
|
|
2343
|
+
{ catalogue: pricing },
|
|
2344
|
+
);
|
|
2345
|
+
|
|
2346
|
+
const server = buildServer({
|
|
2347
|
+
catalogue: pricing,
|
|
2348
|
+
position: () => ({
|
|
2349
|
+
consumedUsd: measured ? report.total.totalUsd : undefined,
|
|
2350
|
+
limitUsd,
|
|
2351
|
+
window: report.span,
|
|
2352
|
+
}),
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
const socket = stringFlag(args, 'socket');
|
|
2356
|
+
const portRaw = stringFlag(args, 'port');
|
|
2357
|
+
const port = portRaw === undefined ? DEFAULT_PORT : Number(portRaw);
|
|
2358
|
+
if (socket === undefined && (!Number.isInteger(port) || port < 0 || port > 65_535)) {
|
|
2359
|
+
throw new Error(t.serve.badPort(String(portRaw)));
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const where = await listen(server, socket !== undefined ? { socket } : { port });
|
|
2363
|
+
console.log(c.bold(t.serve.listening(where)));
|
|
2364
|
+
console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
|
|
2365
|
+
console.log(
|
|
2366
|
+
` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(report.total.totalUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`,
|
|
2367
|
+
);
|
|
2368
|
+
if (limitUsd === undefined) {
|
|
2369
|
+
console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2297
2373
|
/**
|
|
2298
2374
|
* `trazum watch` — the afternoon it happened, said that afternoon.
|
|
2299
2375
|
*
|
|
@@ -7088,6 +7164,9 @@ async function main(): Promise<void> {
|
|
|
7088
7164
|
case 'watch':
|
|
7089
7165
|
await commandWatch(args, config, pricing, t);
|
|
7090
7166
|
break;
|
|
7167
|
+
case 'serve':
|
|
7168
|
+
await commandServe(args, config, pricing, t);
|
|
7169
|
+
break;
|
|
7091
7170
|
case 'route':
|
|
7092
7171
|
await commandRoute(args, pricing, t);
|
|
7093
7172
|
break;
|
package/src/serve.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The endpoint that answers before the call is sent.
|
|
3
|
+
*
|
|
4
|
+
* **Loopback only, and the address is not a flag.** A cost oracle listening on
|
|
5
|
+
* a network interface is an attack surface with a very small upside: it holds
|
|
6
|
+
* a company's spend, its model mix and its budgets, and it answers anybody who
|
|
7
|
+
* asks. `checkedEndpoint` has guarded Trazum's *outbound* requests since 1.14
|
|
8
|
+
* on the principle that a caller selects an endpoint rather than naming one;
|
|
9
|
+
* this is the inbound counterpart, and it is enforced the same way — by there
|
|
10
|
+
* being no way to say otherwise. `127.0.0.1` is compiled in. A Unix socket is
|
|
11
|
+
* offered for callers that would rather not use a port at all.
|
|
12
|
+
*
|
|
13
|
+
* **No auth, on purpose.** Anything reachable only from the machine it runs on
|
|
14
|
+
* is already behind the operating system's own boundary, and a token checked
|
|
15
|
+
* over loopback is theatre: whoever can reach the socket can read the token
|
|
16
|
+
* out of the process that holds it. The honest posture is a surface small
|
|
17
|
+
* enough not to need one.
|
|
18
|
+
*
|
|
19
|
+
* **It degrades rather than failing.** With no store and no budget the
|
|
20
|
+
* endpoint still prices the call from the bundled catalogue and says the
|
|
21
|
+
* budget half is unknown. Offline is a mode, not an error, and an oracle that
|
|
22
|
+
* refuses to speak when half its inputs are missing is an oracle nobody wires
|
|
23
|
+
* into a hot path.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createServer } from 'node:http';
|
|
27
|
+
import type { IncomingMessage, Server, ServerResponse } from 'node:http';
|
|
28
|
+
import { answerCost } from '@trazum/core';
|
|
29
|
+
import type { CostAnswer, PricingCatalogue } from '@trazum/core';
|
|
30
|
+
|
|
31
|
+
/** Compiled in. See the module note: this is the inbound SSRF posture. */
|
|
32
|
+
export const BIND_HOST = '127.0.0.1';
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_PORT = 7317;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bodies larger than this are refused unread.
|
|
38
|
+
*
|
|
39
|
+
* A prompt is text and text is unbounded; a hot-path oracle that will buffer
|
|
40
|
+
* whatever it is handed is a memory exhaustion away from taking the caller
|
|
41
|
+
* down with it — and the caller was asking how to spend *less*.
|
|
42
|
+
*/
|
|
43
|
+
export const MAX_BODY_BYTES = 1_000_000;
|
|
44
|
+
|
|
45
|
+
export interface ServeContext {
|
|
46
|
+
catalogue: PricingCatalogue;
|
|
47
|
+
/**
|
|
48
|
+
* Measured spend and the budget it is judged against, read once at start
|
|
49
|
+
* and refreshed by the caller.
|
|
50
|
+
*
|
|
51
|
+
* Read once because the whole promise here is single-digit milliseconds,
|
|
52
|
+
* and a file read in the request path cannot make that promise. The staleness
|
|
53
|
+
* is a real cost, so the answer carries the window its measurement covers
|
|
54
|
+
* rather than implying it is current to the second.
|
|
55
|
+
*/
|
|
56
|
+
position: () => { consumedUsd?: number; limitUsd?: number; window?: { fromMs: number; toMs: number } | null };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readBody(request: IncomingMessage): Promise<string> {
|
|
60
|
+
const chunks: Buffer[] = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
for await (const chunk of request) {
|
|
63
|
+
size += chunk.length;
|
|
64
|
+
if (size > MAX_BODY_BYTES) throw new Error('body too large');
|
|
65
|
+
chunks.push(chunk as Buffer);
|
|
66
|
+
}
|
|
67
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const send = (response: ServerResponse, status: number, body: unknown): void => {
|
|
71
|
+
const text = JSON.stringify(body);
|
|
72
|
+
response.writeHead(status, {
|
|
73
|
+
'content-type': 'application/json',
|
|
74
|
+
'content-length': Buffer.byteLength(text),
|
|
75
|
+
// Nothing here is for a browser to read across origins, and saying so
|
|
76
|
+
// costs one header.
|
|
77
|
+
'cache-control': 'no-store',
|
|
78
|
+
});
|
|
79
|
+
response.end(text);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function buildServer(context: ServeContext): Server {
|
|
83
|
+
return createServer((request, response) => {
|
|
84
|
+
void (async () => {
|
|
85
|
+
const url = new URL(request.url ?? '/', `http://${BIND_HOST}`);
|
|
86
|
+
|
|
87
|
+
if (request.method === 'GET' && url.pathname === '/health') {
|
|
88
|
+
send(response, 200, { ok: true, schemaVersion: 1 });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (request.method !== 'POST' || url.pathname !== '/cost') {
|
|
93
|
+
send(response, 404, {
|
|
94
|
+
error: 'not-found',
|
|
95
|
+
detail: 'POST /cost, or GET /health.',
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let payload: Record<string, unknown>;
|
|
101
|
+
try {
|
|
102
|
+
const raw = await readBody(request);
|
|
103
|
+
payload = raw.trim() === '' ? {} : (JSON.parse(raw) as Record<string, unknown>);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
send(response, 400, {
|
|
106
|
+
error: 'bad-request',
|
|
107
|
+
detail: error instanceof Error && error.message === 'body too large'
|
|
108
|
+
? `A request body may be at most ${MAX_BODY_BYTES} bytes.`
|
|
109
|
+
: 'The body must be JSON.',
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const position = context.position();
|
|
115
|
+
const answer: CostAnswer = answerCost(
|
|
116
|
+
{
|
|
117
|
+
model: typeof payload.model === 'string' ? payload.model : undefined,
|
|
118
|
+
inputTokens: typeof payload.inputTokens === 'number' ? payload.inputTokens : undefined,
|
|
119
|
+
outputTokens: typeof payload.outputTokens === 'number' ? payload.outputTokens : undefined,
|
|
120
|
+
basis: payload.basis === 'heuristic' ? 'heuristic' : 'token-count',
|
|
121
|
+
...position,
|
|
122
|
+
},
|
|
123
|
+
{ catalogue: context.catalogue },
|
|
124
|
+
);
|
|
125
|
+
send(response, 200, answer);
|
|
126
|
+
})();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ListenTarget {
|
|
131
|
+
/** A Unix socket path, when the caller would rather not use a port. */
|
|
132
|
+
socket?: string;
|
|
133
|
+
port?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function listen(server: Server, target: ListenTarget): Promise<string> {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
server.once('error', reject);
|
|
139
|
+
if (target.socket !== undefined) {
|
|
140
|
+
server.listen(target.socket, () => resolve(target.socket!));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// The host is not a parameter. See the module note.
|
|
144
|
+
server.listen(target.port ?? DEFAULT_PORT, BIND_HOST, () => {
|
|
145
|
+
const address = server.address();
|
|
146
|
+
resolve(typeof address === 'object' && address !== null ? `${BIND_HOST}:${address.port}` : String(address));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|