@trazum/cli 1.50.2 → 1.50.4
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/dist/gateway-server.d.ts +91 -0
- package/dist/gateway-server.d.ts.map +1 -0
- package/dist/gateway-server.js +290 -0
- package/dist/gateway-server.js.map +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +53 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +55 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +33 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +188 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/gateway-server.ts +350 -0
- package/src/i18n/en.ts +69 -0
- package/src/i18n/es.ts +71 -0
- package/src/i18n/types.ts +30 -0
- package/src/index.ts +256 -5
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proxy that stands in the path, and does as little as possible there.
|
|
3
|
+
*
|
|
4
|
+
* The decision lives in `@trazum/core`'s `gatewayDecision`, which never sees a
|
|
5
|
+
* prompt and cannot return a modified request. This file moves bytes: read a
|
|
6
|
+
* body, ask, and either forward it **unchanged** or answer with the refusal.
|
|
7
|
+
* The split is the safety property — everything that could go wrong in a
|
|
8
|
+
* judgement is tested without a socket, and everything that could go wrong on
|
|
9
|
+
* a socket has no judgement in it.
|
|
10
|
+
*
|
|
11
|
+
* **Loopback only, and the address is not a flag.** Same posture as `serve`
|
|
12
|
+
* since 1.44, and more load-bearing here: this thing has somebody's provider
|
|
13
|
+
* credential passing through it. `127.0.0.1` is compiled in.
|
|
14
|
+
*
|
|
15
|
+
* **The credential is not even borrowed.** The caller's own `authorization`
|
|
16
|
+
* and `x-api-key` headers are forwarded untouched and never read, never
|
|
17
|
+
* stored, never logged, and never put in a URL. Trazum holds no key for the
|
|
18
|
+
* gateway and has no way to make a call of its own through it — which is a
|
|
19
|
+
* stronger promise than the connector's *borrowed, never held*, and the right
|
|
20
|
+
* one for a component sitting between somebody and their provider.
|
|
21
|
+
*
|
|
22
|
+
* **The upstream is compiled in.** A flag naming the host would turn this into
|
|
23
|
+
* a credential-forwarding open proxy: anything that could rewrite a config on
|
|
24
|
+
* disk could point a company's API key at a machine it chose. `checkedEndpoint`
|
|
25
|
+
* has guarded Trazum's outbound calls on that principle since 1.14, and here
|
|
26
|
+
* there is no caller-supplied endpoint at all.
|
|
27
|
+
*
|
|
28
|
+
* **Nothing about the payload is written down.** The body is read to count
|
|
29
|
+
* tokens and to find the model, then forwarded and dropped. It is never
|
|
30
|
+
* logged, never stored, and never included in a refusal — the store has held
|
|
31
|
+
* aggregates since 1.42 and standing in the path changes nothing about that.
|
|
32
|
+
*/
|
|
33
|
+
import type { Server } from 'node:http';
|
|
34
|
+
import type { GatewayPolicy, GatewayStanding, PricingCatalogue } from '@trazum/core';
|
|
35
|
+
/** Compiled in. See the module note. */
|
|
36
|
+
export declare const BIND_HOST = "127.0.0.1";
|
|
37
|
+
export declare const DEFAULT_GATEWAY_PORT = 7318;
|
|
38
|
+
/**
|
|
39
|
+
* Bodies larger than this are refused unread.
|
|
40
|
+
*
|
|
41
|
+
* Larger than `serve`'s limit because a real request carries a real prompt,
|
|
42
|
+
* and smaller than unbounded because a proxy that buffers whatever it is
|
|
43
|
+
* handed is a memory exhaustion away from taking down the application it was
|
|
44
|
+
* installed to protect.
|
|
45
|
+
*/
|
|
46
|
+
export declare const MAX_GATEWAY_BODY_BYTES: number;
|
|
47
|
+
/**
|
|
48
|
+
* Where each provider actually is, and the one path this speaks for it.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrow. A gateway that forwarded any path would be a general
|
|
51
|
+
* proxy for somebody's API key, and the budget decision only has meaning for
|
|
52
|
+
* the endpoint that spends tokens.
|
|
53
|
+
*/
|
|
54
|
+
export declare const UPSTREAMS: Readonly<Record<string, {
|
|
55
|
+
origin: string;
|
|
56
|
+
path: string;
|
|
57
|
+
}>>;
|
|
58
|
+
export interface GatewayContext {
|
|
59
|
+
provider: string;
|
|
60
|
+
catalogue: PricingCatalogue;
|
|
61
|
+
policy: GatewayPolicy;
|
|
62
|
+
/** Where the budget stands, refreshed by the caller — never read per request. */
|
|
63
|
+
standing: () => GatewayStanding | null;
|
|
64
|
+
/**
|
|
65
|
+
* Called after a forwarded call returns, with the provider's own counts.
|
|
66
|
+
*
|
|
67
|
+
* Counts only. There is no parameter here that could carry a prompt, which
|
|
68
|
+
* is what makes "nothing about the payload is written down" a fact about the
|
|
69
|
+
* interface rather than a discipline.
|
|
70
|
+
*/
|
|
71
|
+
record: (measured: {
|
|
72
|
+
model: string;
|
|
73
|
+
label: string | null;
|
|
74
|
+
inputTokens: number;
|
|
75
|
+
outputTokens: number;
|
|
76
|
+
cacheReadTokens: number;
|
|
77
|
+
cacheWriteTokens: number;
|
|
78
|
+
substituted: boolean;
|
|
79
|
+
}) => void;
|
|
80
|
+
/** A line for the operator's terminal. Never given a body, ever. */
|
|
81
|
+
note: (line: string) => void;
|
|
82
|
+
/** Injected so the proxy is testable against a stub upstream. */
|
|
83
|
+
fetchImpl?: typeof fetch;
|
|
84
|
+
}
|
|
85
|
+
export declare function buildGateway(context: GatewayContext): Server;
|
|
86
|
+
export declare function listenGateway(server: Server, where: {
|
|
87
|
+
port: number;
|
|
88
|
+
} | {
|
|
89
|
+
socket: string;
|
|
90
|
+
}): Promise<string>;
|
|
91
|
+
//# sourceMappingURL=gateway-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-server.d.ts","sourceRoot":"","sources":["../src/gateway-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,OAAO,KAAK,EAAmB,MAAM,EAAkB,MAAM,WAAW,CAAC;AAEzE,OAAO,KAAK,EAAmB,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEtG,wCAAwC;AACxC,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AAEtD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAGhF,CAAC;AAmBF,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;IACvC;;;;;;OAMG;IACH,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,eAAe,EAAE,MAAM,CAAC;QACxB,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,EAAE,OAAO,CAAC;KACtB,KAAK,IAAI,CAAC;IACX,oEAAoE;IACpE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AA+FD,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAgI5D;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C,OAAO,CAAC,MAAM,CAAC,CAajB"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proxy that stands in the path, and does as little as possible there.
|
|
3
|
+
*
|
|
4
|
+
* The decision lives in `@trazum/core`'s `gatewayDecision`, which never sees a
|
|
5
|
+
* prompt and cannot return a modified request. This file moves bytes: read a
|
|
6
|
+
* body, ask, and either forward it **unchanged** or answer with the refusal.
|
|
7
|
+
* The split is the safety property — everything that could go wrong in a
|
|
8
|
+
* judgement is tested without a socket, and everything that could go wrong on
|
|
9
|
+
* a socket has no judgement in it.
|
|
10
|
+
*
|
|
11
|
+
* **Loopback only, and the address is not a flag.** Same posture as `serve`
|
|
12
|
+
* since 1.44, and more load-bearing here: this thing has somebody's provider
|
|
13
|
+
* credential passing through it. `127.0.0.1` is compiled in.
|
|
14
|
+
*
|
|
15
|
+
* **The credential is not even borrowed.** The caller's own `authorization`
|
|
16
|
+
* and `x-api-key` headers are forwarded untouched and never read, never
|
|
17
|
+
* stored, never logged, and never put in a URL. Trazum holds no key for the
|
|
18
|
+
* gateway and has no way to make a call of its own through it — which is a
|
|
19
|
+
* stronger promise than the connector's *borrowed, never held*, and the right
|
|
20
|
+
* one for a component sitting between somebody and their provider.
|
|
21
|
+
*
|
|
22
|
+
* **The upstream is compiled in.** A flag naming the host would turn this into
|
|
23
|
+
* a credential-forwarding open proxy: anything that could rewrite a config on
|
|
24
|
+
* disk could point a company's API key at a machine it chose. `checkedEndpoint`
|
|
25
|
+
* has guarded Trazum's outbound calls on that principle since 1.14, and here
|
|
26
|
+
* there is no caller-supplied endpoint at all.
|
|
27
|
+
*
|
|
28
|
+
* **Nothing about the payload is written down.** The body is read to count
|
|
29
|
+
* tokens and to find the model, then forwarded and dropped. It is never
|
|
30
|
+
* logged, never stored, and never included in a refusal — the store has held
|
|
31
|
+
* aggregates since 1.42 and standing in the path changes nothing about that.
|
|
32
|
+
*/
|
|
33
|
+
import { createServer } from 'node:http';
|
|
34
|
+
import { estimateTokens, gatewayDecision, usageFromResponse } from '@trazum/core';
|
|
35
|
+
/** Compiled in. See the module note. */
|
|
36
|
+
export const BIND_HOST = '127.0.0.1';
|
|
37
|
+
export const DEFAULT_GATEWAY_PORT = 7318;
|
|
38
|
+
/**
|
|
39
|
+
* Bodies larger than this are refused unread.
|
|
40
|
+
*
|
|
41
|
+
* Larger than `serve`'s limit because a real request carries a real prompt,
|
|
42
|
+
* and smaller than unbounded because a proxy that buffers whatever it is
|
|
43
|
+
* handed is a memory exhaustion away from taking down the application it was
|
|
44
|
+
* installed to protect.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_GATEWAY_BODY_BYTES = 8 * 1024 * 1024;
|
|
47
|
+
/**
|
|
48
|
+
* Where each provider actually is, and the one path this speaks for it.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrow. A gateway that forwarded any path would be a general
|
|
51
|
+
* proxy for somebody's API key, and the budget decision only has meaning for
|
|
52
|
+
* the endpoint that spends tokens.
|
|
53
|
+
*/
|
|
54
|
+
export const UPSTREAMS = {
|
|
55
|
+
anthropic: { origin: 'https://api.anthropic.com', path: '/v1/messages' },
|
|
56
|
+
openai: { origin: 'https://api.openai.com', path: '/v1/chat/completions' },
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Headers Trazum adds or removes. Everything else the caller sent is forwarded
|
|
60
|
+
* verbatim, including their credential, which this never reads.
|
|
61
|
+
*/
|
|
62
|
+
const HOP_BY_HOP = new Set([
|
|
63
|
+
'connection',
|
|
64
|
+
'keep-alive',
|
|
65
|
+
'proxy-authenticate',
|
|
66
|
+
'proxy-authorization',
|
|
67
|
+
'te',
|
|
68
|
+
'trailer',
|
|
69
|
+
'transfer-encoding',
|
|
70
|
+
'upgrade',
|
|
71
|
+
'host',
|
|
72
|
+
'content-length',
|
|
73
|
+
]);
|
|
74
|
+
async function readBody(request) {
|
|
75
|
+
const chunks = [];
|
|
76
|
+
let size = 0;
|
|
77
|
+
for await (const chunk of request) {
|
|
78
|
+
const buffer = Buffer.from(chunk);
|
|
79
|
+
size += buffer.length;
|
|
80
|
+
if (size > MAX_GATEWAY_BODY_BYTES)
|
|
81
|
+
return null;
|
|
82
|
+
chunks.push(buffer);
|
|
83
|
+
}
|
|
84
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* What the request is asking for, without keeping any of it.
|
|
88
|
+
*
|
|
89
|
+
* The token count is the heuristic estimator's — the same one every other
|
|
90
|
+
* estimate in this product uses, with the same documented error band. Counting
|
|
91
|
+
* exactly would mean an API call to count before the API call, which is a
|
|
92
|
+
* round trip in a hot path to make a budget decision marginally sharper.
|
|
93
|
+
*
|
|
94
|
+
* The returned object holds no text. That is the point: everything downstream
|
|
95
|
+
* of here, including the decision and the record, is structurally incapable of
|
|
96
|
+
* carrying a prompt.
|
|
97
|
+
*/
|
|
98
|
+
function describe(body, provider) {
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(body);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
107
|
+
return null;
|
|
108
|
+
const request = parsed;
|
|
109
|
+
if (typeof request.model !== 'string')
|
|
110
|
+
return null;
|
|
111
|
+
// Every text field the wire format puts in front of the model, counted and
|
|
112
|
+
// then dropped. `JSON.stringify` of the messages over-counts by the
|
|
113
|
+
// structural characters, which is the safe direction for a budget: an
|
|
114
|
+
// estimate that runs high refuses slightly early rather than allowing
|
|
115
|
+
// slightly late.
|
|
116
|
+
const parts = [];
|
|
117
|
+
if (typeof request.system === 'string')
|
|
118
|
+
parts.push(request.system);
|
|
119
|
+
if (Array.isArray(request.messages))
|
|
120
|
+
parts.push(JSON.stringify(request.messages));
|
|
121
|
+
if (Array.isArray(request.input))
|
|
122
|
+
parts.push(JSON.stringify(request.input));
|
|
123
|
+
const text = parts.join('\n');
|
|
124
|
+
const max = provider === 'anthropic' ? request.max_tokens : request.max_completion_tokens ?? request.max_tokens;
|
|
125
|
+
return {
|
|
126
|
+
model: request.model,
|
|
127
|
+
inputTokens: text === '' ? null : estimateTokens(text),
|
|
128
|
+
maxOutputTokens: typeof max === 'number' && Number.isFinite(max) ? max : null,
|
|
129
|
+
/**
|
|
130
|
+
* `metadata.trazum_label`, and nothing inferred.
|
|
131
|
+
*
|
|
132
|
+
* A label is what makes a per-workload bill possible, and guessing one
|
|
133
|
+
* from a path or a user agent would attribute somebody's spend to a
|
|
134
|
+
* workload they never named.
|
|
135
|
+
*/
|
|
136
|
+
label: labelOf(request),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function labelOf(request) {
|
|
140
|
+
const metadata = request.metadata;
|
|
141
|
+
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata))
|
|
142
|
+
return null;
|
|
143
|
+
const label = metadata.trazum_label;
|
|
144
|
+
return typeof label === 'string' && label.trim() !== '' ? label : null;
|
|
145
|
+
}
|
|
146
|
+
/** The refusal, as the caller's SDK will receive it. */
|
|
147
|
+
function refusalBody(decision) {
|
|
148
|
+
return `${JSON.stringify({
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
error: { type: 'trazum_budget_refusal', message: decision.because },
|
|
151
|
+
reason: decision.reason,
|
|
152
|
+
cause: decision.cause,
|
|
153
|
+
restsOn: decision.restsOn,
|
|
154
|
+
standing: decision.standing,
|
|
155
|
+
estimatedUsd: decision.estimatedUsd,
|
|
156
|
+
alternatives: decision.alternatives,
|
|
157
|
+
}, null, 2)}\n`;
|
|
158
|
+
}
|
|
159
|
+
export function buildGateway(context) {
|
|
160
|
+
const upstream = UPSTREAMS[context.provider];
|
|
161
|
+
const doFetch = context.fetchImpl ?? fetch;
|
|
162
|
+
return createServer((request, response) => {
|
|
163
|
+
void (async () => {
|
|
164
|
+
if (upstream === undefined) {
|
|
165
|
+
response.writeHead(500, { 'content-type': 'application/json' });
|
|
166
|
+
response.end(`${JSON.stringify({ error: 'no upstream configured for this provider' })}\n`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (request.method !== 'POST' || request.url !== upstream.path) {
|
|
170
|
+
// Only the one path that spends tokens. A gateway forwarding anything
|
|
171
|
+
// else is a general proxy for somebody's API key.
|
|
172
|
+
response.writeHead(404, { 'content-type': 'application/json' });
|
|
173
|
+
response.end(`${JSON.stringify({ error: 'not a path this gateway forwards' })}\n`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const body = await readBody(request);
|
|
177
|
+
if (body === null) {
|
|
178
|
+
response.writeHead(413, { 'content-type': 'application/json' });
|
|
179
|
+
response.end(`${JSON.stringify({ error: 'request body too large' })}\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const described = describe(body, context.provider);
|
|
183
|
+
if (described === null) {
|
|
184
|
+
response.writeHead(400, { 'content-type': 'application/json' });
|
|
185
|
+
response.end(`${JSON.stringify({ error: 'could not read a model out of this request' })}\n`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const decision = gatewayDecision({ provider: context.provider, ...described }, context.standing(), { catalogue: context.catalogue, policy: context.policy });
|
|
189
|
+
if (decision.kind === 'refuse') {
|
|
190
|
+
/**
|
|
191
|
+
* **402, deliberately, and never 429.**
|
|
192
|
+
*
|
|
193
|
+
* Every provider SDK retries a 429 automatically — that is what the
|
|
194
|
+
* code means to them — so answering a budget refusal with one turns a
|
|
195
|
+
* single refusal into a retry storm against a gateway that will refuse
|
|
196
|
+
* every time. 402 Payment Required is both literally correct and in
|
|
197
|
+
* nobody's default retry list.
|
|
198
|
+
*/
|
|
199
|
+
response.writeHead(402, { 'content-type': 'application/json' });
|
|
200
|
+
response.end(refusalBody(decision));
|
|
201
|
+
context.note(`refused ${described.model}: ${decision.reason}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const outgoing = new Headers();
|
|
205
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
206
|
+
if (HOP_BY_HOP.has(name.toLowerCase()) || value === undefined)
|
|
207
|
+
continue;
|
|
208
|
+
outgoing.set(name, Array.isArray(value) ? value.join(', ') : value);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The body forwarded is the body received, **byte for byte**, except on
|
|
212
|
+
* a configured substitution — which replaces exactly one field and says
|
|
213
|
+
* so in the record.
|
|
214
|
+
*/
|
|
215
|
+
let forwarded = body;
|
|
216
|
+
if (decision.kind === 'substitute') {
|
|
217
|
+
const parsed = JSON.parse(body);
|
|
218
|
+
parsed.model = decision.to.id;
|
|
219
|
+
forwarded = JSON.stringify(parsed);
|
|
220
|
+
context.note(`substituted ${described.model} → ${decision.to.id}: ${decision.configuredReason}`);
|
|
221
|
+
}
|
|
222
|
+
else if (decision.unjudged !== null) {
|
|
223
|
+
context.note(`forwarded unjudged (${decision.unjudged}): fail-open`);
|
|
224
|
+
}
|
|
225
|
+
let upstreamResponse;
|
|
226
|
+
try {
|
|
227
|
+
upstreamResponse = await doFetch(`${upstream.origin}${upstream.path}`, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: outgoing,
|
|
230
|
+
body: forwarded,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
/**
|
|
235
|
+
* The upstream is unreachable. This is **not** a budget refusal and
|
|
236
|
+
* must not look like one: the caller needs to tell "your provider is
|
|
237
|
+
* down" from "you are out of money", and a proxy that blurs them sends
|
|
238
|
+
* somebody to fix the wrong thing.
|
|
239
|
+
*/
|
|
240
|
+
response.writeHead(502, { 'content-type': 'application/json' });
|
|
241
|
+
response.end(`${JSON.stringify({
|
|
242
|
+
error: { type: 'trazum_upstream_unreachable', message: error instanceof Error ? error.message : String(error) },
|
|
243
|
+
})}\n`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const text = await upstreamResponse.text();
|
|
247
|
+
// Measured at the moment of the call, from the provider's own counts —
|
|
248
|
+
// the reason this beats a connector, which reports the runaway after it
|
|
249
|
+
// ran. Counts only reach `record`; the body is dropped here.
|
|
250
|
+
let measured;
|
|
251
|
+
try {
|
|
252
|
+
measured = JSON.parse(text);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
measured = null;
|
|
256
|
+
}
|
|
257
|
+
const usage = usageFromResponse(context.provider, measured);
|
|
258
|
+
if (usage !== null) {
|
|
259
|
+
context.record({
|
|
260
|
+
model: decision.kind === 'substitute' ? decision.to.id : described.model,
|
|
261
|
+
label: described.label,
|
|
262
|
+
substituted: decision.kind === 'substitute',
|
|
263
|
+
...usage,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const back = {};
|
|
267
|
+
upstreamResponse.headers.forEach((value, name) => {
|
|
268
|
+
if (!HOP_BY_HOP.has(name.toLowerCase()))
|
|
269
|
+
back[name] = value;
|
|
270
|
+
});
|
|
271
|
+
response.writeHead(upstreamResponse.status, back);
|
|
272
|
+
response.end(text);
|
|
273
|
+
})();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
export function listenGateway(server, where) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
server.once('error', reject);
|
|
279
|
+
if ('socket' in where) {
|
|
280
|
+
server.listen(where.socket, () => resolve(where.socket));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
server.listen(where.port, BIND_HOST, () => {
|
|
284
|
+
const address = server.address();
|
|
285
|
+
const port = typeof address === 'object' && address !== null ? address.port : where.port;
|
|
286
|
+
resolve(`http://${BIND_HOST}:${port}`);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=gateway-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-server.js","sourceRoot":"","sources":["../src/gateway-server.ts"],"names":[],"mappings":"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"}
|
package/dist/i18n/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAqB9C;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAqB9C;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,EAAE,WAmiEhB,CAAC"}
|
package/dist/i18n/en.js
CHANGED
|
@@ -48,6 +48,7 @@ ${bold('USAGE')}
|
|
|
48
48
|
trazum conform <file|-> [--contract <name>]
|
|
49
49
|
trazum models
|
|
50
50
|
trazum rules
|
|
51
|
+
trazum gateway <anthropic|openai> --on-cannot-tell <fail-open|fail-closed>
|
|
51
52
|
trazum feedback
|
|
52
53
|
trazum --version
|
|
53
54
|
|
|
@@ -92,6 +93,22 @@ ${bold('OPTIONS FOR feedback')}
|
|
|
92
93
|
anonymous counter, and a test fails the build if this command ever reaches
|
|
93
94
|
the network.
|
|
94
95
|
|
|
96
|
+
${bold('OPTIONS FOR gateway')}
|
|
97
|
+
--on-cannot-tell <policy> Required, no default: fail-open or fail-closed.
|
|
98
|
+
What happens when the gateway cannot judge a call
|
|
99
|
+
— no budget, nothing measured, an unpriced model.
|
|
100
|
+
--port <n> | --socket <p> Where to listen. Loopback only, always.
|
|
101
|
+
|
|
102
|
+
Stands between your SDK and the provider, speaking their wire format, so no
|
|
103
|
+
code changes. Usage is measured from the provider's own response as it comes
|
|
104
|
+
back — no export, no connector lag, no missing day.
|
|
105
|
+
|
|
106
|
+
It refuses and never substitutes: a call over budget gets HTTP 402 with the
|
|
107
|
+
cheaper alternatives named. Substitution happens only where you wrote it down
|
|
108
|
+
in spend.substitute, with your reason, and every substituted call is marked.
|
|
109
|
+
|
|
110
|
+
Your credential is forwarded untouched and never read. See docs/gateway.md.
|
|
111
|
+
|
|
95
112
|
${bold('OPTIONS FOR prune')}
|
|
96
113
|
--cases <file> One input per line, or a JSON array. Required.
|
|
97
114
|
--yes Actually spend the calls. Without it the estimate is
|
|
@@ -571,6 +588,13 @@ ${bold('CONFIG FILE')}
|
|
|
571
588
|
spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — money budgets for
|
|
572
589
|
"trazum profile", in dollars. A budgeted label with no calls in
|
|
573
590
|
the log is reported as not measured, never as a pass
|
|
591
|
+
outcomes { "values": ["resolved", "escalated"], "success": ["resolved"] } —
|
|
592
|
+
your own vocabulary for what happened, and which of it counts as
|
|
593
|
+
a win. Both required: which words mean success is a judgement
|
|
594
|
+
about your product rather than your bill, and this tool has no
|
|
595
|
+
standing to make it. Use [] if none of them are successes. A
|
|
596
|
+
value in a log that "values" never declares is named as
|
|
597
|
+
undeclared, never counted as a failure
|
|
574
598
|
waive [{ "gate": "maxUsd", "reason": "August migration", "until":
|
|
575
599
|
"2026-09-15" }] — a gate failure decided about, on the record.
|
|
576
600
|
All three fields required: a waiver with no end date is a
|
|
@@ -840,6 +864,22 @@ ${bold('EXAMPLES')}
|
|
|
840
864
|
existingRefused: (path) => `${path} already exists and was left alone. Pass --dry-run to see what would go in it, or --yes to replace it.`,
|
|
841
865
|
existingUnparseable: (path) => `${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
|
|
842
866
|
},
|
|
867
|
+
gateway: {
|
|
868
|
+
badProvider: (given, known) => given === ''
|
|
869
|
+
? `Name the provider to stand in front of. Known: ${known}.`
|
|
870
|
+
: `"${given}" is not a provider this gateway speaks for. Known: ${known}.`,
|
|
871
|
+
needsPolicy: (policies) => `--on-cannot-tell is required, and there is no default: ${policies}. When the gateway cannot judge a call — no budget, nothing measured, an unpriced model — one of these happens, and only you know which failure your product can survive. fail-open keeps it working and lets the bill run; fail-closed stops the bill and takes it down with it. Picking one for you would be the most consequential decision in your architecture, made silently at install time.`,
|
|
872
|
+
listening: (where, provider) => `Gateway on ${where}, in front of ${provider}`,
|
|
873
|
+
pointYourSdk: (where) => `Point your SDK's base URL at ${where} and change nothing else. It speaks the provider's own wire format, so no code changes and no new client.`,
|
|
874
|
+
credential: () => 'Your credential is forwarded untouched and never read, never stored, never logged and never put in a URL. Trazum holds no key here and cannot make a call of its own through this.',
|
|
875
|
+
neverSubstitutes: () => 'A call over budget is refused with HTTP 402 and the cheaper alternatives named — never silently swapped, trimmed or downgraded. 402 rather than 429 on purpose: every provider SDK retries a 429, which would turn one refusal into a retry storm.',
|
|
876
|
+
standing: (consumed, limit) => `Judging against ${consumed} of ${limit}, measured, read once at start — a file read in the request path would put this tool's latency between you and your provider on every call.`,
|
|
877
|
+
noStanding: () => 'Nothing measured for this period, so every call is unjudged and the failure policy below decides. Set spend.monthlyUsd and pull with trazum connect.',
|
|
878
|
+
policy: (policy) => policy === 'fail-open'
|
|
879
|
+
? 'When it cannot judge: the call goes through, and the record says it was unjudged rather than within budget.'
|
|
880
|
+
: 'When it cannot judge: the call is refused. Nothing gets through unmeasured.',
|
|
881
|
+
measured: (model, label, input, output, substituted) => ` ${model}${label === null ? '' : ` [${label}]`}: ${input} in, ${output} out${substituted ? ' (substituted — marked, and never counted as the call that was asked for)' : ''}`,
|
|
882
|
+
},
|
|
843
883
|
feedback: {
|
|
844
884
|
heading: () => 'Telling us something',
|
|
845
885
|
sendsNothing: () => 'This command sends nothing, and neither does anything else here. Trazum has no telemetry: no ping, no install hook, no anonymous counter. A tool whose whole argument is that it reads your bill without uploading it cannot also be quietly reporting on you, and a test fails the build if this command ever reaches the network.',
|
|
@@ -1287,6 +1327,19 @@ ${bold('EXAMPLES')}
|
|
|
1287
1327
|
fleetBudgetMissing: (name) => `${name} has a budget in spend.bySource and no logs matched it in this run, so nothing was measured for it. Not a pass: a service that did not appear is not a service under budget.`,
|
|
1288
1328
|
coverageHeading: () => 'What this log cannot answer yet',
|
|
1289
1329
|
needsLabel: (seen) => `"label" on ${seen} records: without it every workload is one row, so no per-workload spend, no drill-down, and the levers describe a mixture rather than a decision.`,
|
|
1330
|
+
needsOutcome: (seen) => `an "outcome" — ${seen}. The one field that changes what every other figure here means: without it this tool can say a workload got 40% cheaper and cannot say whether it stopped working. Record your own word for what happened and declare the vocabulary under "outcomes".`,
|
|
1331
|
+
dryRunOutcomes: (share) => `cost per outcome and a success rate (${share} of records carry an "outcome")`,
|
|
1332
|
+
outcomeHeading: () => 'Outcomes',
|
|
1333
|
+
outcomeRate: (rate, ofUsd) => `${rate} of ${ofUsd} in declared outcomes succeeded \u2014 by spend rather than by call, because the two diverge exactly when the expensive half is the half that fails.`,
|
|
1334
|
+
outcomeNoRate: (why) => why === 'nothing-recorded'
|
|
1335
|
+
? 'No success rate: nothing in this log recorded an outcome. That is not a rate of zero \u2014 a rate of zero is a real and terrible measurement, and this is nobody having told us.'
|
|
1336
|
+
: 'No success rate: "outcomes.success" declares no values, so nothing here counts as one. A legitimate thing to declare, and it means the rate is not this tool\u2019s to compute.',
|
|
1337
|
+
outcomeUnrecorded: (share, usd) => `${share} of the bill (${usd}) carried no outcome, and is in neither half of the rate above.`,
|
|
1338
|
+
outcomeUndeclared: (values) => `Not declared in "outcomes.values": ${values}. Named rather than counted as failures \u2014 a typo in an exporter should look like a typo, not like a product regression.`,
|
|
1339
|
+
outcomeColumns: { outcome: 'outcome', calls: 'calls', spend: 'spend' },
|
|
1340
|
+
verdictSuccess: () => 'success',
|
|
1341
|
+
verdictOther: () => '\u2014',
|
|
1342
|
+
verdictUndeclared: () => 'undeclared',
|
|
1290
1343
|
needsSession: (seen) => `"session" on ${seen} records: without it there is no conversation growth, no per-conversation cost, and no cache-TTL fit. It is grouped by and never printed.`,
|
|
1291
1344
|
needsTs: (seen) => `"ts" on ${seen} records: without it the log has no period, no per-day or per-hour shape, and the cache-TTL question cannot be asked at all.`,
|
|
1292
1345
|
needsStopReason: (seen) => `"stop_reason" (Anthropic) or "finish_reason" (OpenAI) on ${seen} records: without it, answers cut off at max_tokens are invisible — and silence there is not the same as none.`,
|