@urun-sh/openai 0.5.5 → 0.6.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/dist/{ResponsesClient-BYx3YLGo.d.ts → ResponsesClient-CSSrOYD8.d.ts} +7 -1
- package/dist/{ResponsesClient-Dft3bg3b.d.cts → ResponsesClient-_OZERUjH.d.cts} +11 -1
- package/dist/chunk-3JWYIKHM.js +2 -0
- package/dist/chunk-45FJ5GTP.js +7 -0
- package/dist/chunk-6M5JK4YC.js +1 -0
- package/dist/chunk-QTVTCMJU.js +1 -0
- package/dist/chunk-TDRZZGUK.js +1 -0
- package/dist/chunk-TPH77ZOW.js +58 -0
- package/dist/chunk-WCMX5VOF.js +4 -0
- package/dist/chunk-YXK42SKC.js +1 -0
- package/dist/gemini-live.cjs +2 -2
- package/dist/gemini-live.d.cts +79 -7
- package/dist/gemini-live.d.ts +32 -7
- package/dist/gemini-live.js +1 -1
- package/dist/hosted/bin.cjs +43 -27
- package/dist/hosted/bin.js +4 -4
- package/dist/hosted/index.cjs +41 -26
- package/dist/hosted/index.d.cts +710 -17
- package/dist/hosted/index.d.ts +171 -6
- package/dist/hosted/index.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/dist/{models-NYMZrklp.d.cts → models-DUdx_Y6X.d.cts} +1 -1
- package/dist/pi-extension/index.cjs +7 -7
- package/dist/pi-extension/index.d.cts +2 -2
- package/dist/pi-extension/index.d.ts +2 -2
- package/dist/pi-extension/index.js +1 -1
- package/dist/pi-extension/standalone.cjs +53 -53
- package/dist/proxy/cli.cjs +55 -43
- package/dist/proxy/cli.js +14 -13
- package/dist/proxy/index.cjs +31 -28
- package/dist/proxy/index.d.cts +132 -18
- package/dist/proxy/index.d.ts +28 -11
- package/dist/proxy/index.js +1 -6
- package/dist/responses-turn-BfdBbey8.d.cts +2394 -0
- package/dist/responses-turn-CdhIre_a.d.ts +652 -0
- package/dist/{translator-CcDBEfvm.d.cts → translator-CO8W_hmJ.d.cts} +3 -3
- package/dist/{translator-C9uPKypK.d.ts → translator-Ddd65sXR.d.ts} +1 -1
- package/dist/{types-lsVTbNcH.d.cts → types-CHPtJx6d.d.cts} +13 -0
- package/dist/{types-lsVTbNcH.d.ts → types-CHPtJx6d.d.ts} +4 -0
- package/dist/{video-out-D20UuJ8G.d.cts → video-out-BsuLqlID.d.cts} +6 -6
- package/dist/{video-out-CWesbk12.d.ts → video-out-Cawtm7SF.d.ts} +1 -1
- package/package.json +14 -11
- package/dist/chunk-5BZCM3RS.js +0 -4
- package/dist/chunk-5NXM4IO3.js +0 -2
- package/dist/chunk-CWJRDBDC.js +0 -1
- package/dist/chunk-DMD5UENK.js +0 -1
- package/dist/chunk-GBBY3PCZ.js +0 -1
- package/dist/chunk-I2Q3B3OG.js +0 -6
- package/dist/chunk-OI2OY32M.js +0 -1
- package/dist/chunk-QVF7NF7G.js +0 -44
- package/dist/responses-turn-KOAoIqZ-.d.ts +0 -200
- package/dist/responses-turn-OrO4euEN.d.cts +0 -513
- /package/dist/{models-NYMZrklp.d.ts → models-DUdx_Y6X.d.ts} +0 -0
package/dist/hosted/index.d.cts
CHANGED
|
@@ -1,10 +1,147 @@
|
|
|
1
1
|
import { Server } from 'node:http';
|
|
2
|
+
import * as _prometheus_io_client from '@prometheus-io/client';
|
|
3
|
+
import { Registry } from '@prometheus-io/client';
|
|
4
|
+
import { c as LedgerOutcome, I as InferenceRecord, S as SessionGoneError, P as ProxyClients, M as ModelRouter, i as UsageRowReporter, b as InferenceUsageRecord, e as LedgerWrite, d as LedgerRecorded, L as LedgerFailure } from '../responses-turn-BfdBbey8.cjs';
|
|
2
5
|
import { createClientToken } from '@urun-sh/core';
|
|
3
|
-
import { U as UrunResponses } from '../ResponsesClient-
|
|
4
|
-
import { b as UrunSessionLike } from '../types-
|
|
5
|
-
import
|
|
6
|
-
import '../
|
|
7
|
-
|
|
6
|
+
import { U as UrunResponses } from '../ResponsesClient-_OZERUjH.cjs';
|
|
7
|
+
import { b as UrunSessionLike } from '../types-CHPtJx6d.cjs';
|
|
8
|
+
import '../models-DUdx_Y6X.cjs';
|
|
9
|
+
import '../video-out-BsuLqlID.cjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The container's metrics port — a MODULE CONSTANT, not an env var, per the
|
|
13
|
+
* hosted config mandate (`hosted/config.ts`: "env vars are NOT a config
|
|
14
|
+
* mechanism"). 9464 is the Prometheus exporter port the OpenTelemetry
|
|
15
|
+
* ecosystem registered, and it is the interface contract the deployment chart
|
|
16
|
+
* and its ServiceMonitor are built against.
|
|
17
|
+
*
|
|
18
|
+
* PRIVATE, and private STRUCTURALLY rather than by convention: the public
|
|
19
|
+
* HTTPRoute allowlist (`k8s-manifests` `_public-matches.tpl`) routes only
|
|
20
|
+
* `/v1` to the Service's 8080 port, so nothing on this port is reachable from
|
|
21
|
+
* the internet. It is served by its OWN `http.Server`
|
|
22
|
+
* ({@link createMetricsServer}) rather than as a route on the main server, so
|
|
23
|
+
* a future public path can never accidentally expose it.
|
|
24
|
+
*/
|
|
25
|
+
declare const METRICS_PORT = 9464;
|
|
26
|
+
/** The path the ServiceMonitor scrapes. */
|
|
27
|
+
declare const METRICS_PATH = "/metrics";
|
|
28
|
+
/** The label value every bounded label collapses to once its budget is spent. */
|
|
29
|
+
declare const OTHER_LABEL = "(other)";
|
|
30
|
+
/** The label value for a request that named no model. */
|
|
31
|
+
declare const NO_MODEL_LABEL = "(none)";
|
|
32
|
+
/**
|
|
33
|
+
* Distinct `model` values that get their own series. Above this the label
|
|
34
|
+
* collapses to {@link OTHER_LABEL} — a caller sending a fresh random model
|
|
35
|
+
* string per request must not be able to grow this process's series set (and
|
|
36
|
+
* therefore Prometheus's) without bound.
|
|
37
|
+
*/
|
|
38
|
+
declare const MAX_MODEL_LABELS = 64;
|
|
39
|
+
/**
|
|
40
|
+
* Distinct `org` values that get their own series. Bounded for the same reason
|
|
41
|
+
* even though org ids come from the control plane rather than the caller: the
|
|
42
|
+
* number of orgs is unbounded over time, and a metric's cardinality must be a
|
|
43
|
+
* property of the code, not of how successful sales were.
|
|
44
|
+
*/
|
|
45
|
+
declare const MAX_ORG_LABELS = 128;
|
|
46
|
+
/** Collapse a client-supplied path onto the bounded lane label. */
|
|
47
|
+
declare function laneOf(path: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* The status class label. `none` is its own class rather than being folded
|
|
50
|
+
* into 5xx: it means NO head ever flushed (an aborted upload, a client that
|
|
51
|
+
* vanished during the body read), which `inferenceRequestRecord` records as a
|
|
52
|
+
* null status precisely so nothing downstream invents a 200 the client was
|
|
53
|
+
* never sent.
|
|
54
|
+
*/
|
|
55
|
+
declare function statusClassOf(status: number | null): string;
|
|
56
|
+
/**
|
|
57
|
+
* A label whose distinct-value budget is FIXED at construction. The first
|
|
58
|
+
* `max` distinct values are ADMITTED; everything after is {@link OTHER_LABEL},
|
|
59
|
+
* permanently. Deliberately NOT an LRU: an LRU would let a flooding caller
|
|
60
|
+
* evict the real models and quietly rewrite history in the TSDB, where each
|
|
61
|
+
* admitted-then-evicted-then-readmitted value leaves a broken series behind.
|
|
62
|
+
*
|
|
63
|
+
* ADMISSION IS EARNED, AND THAT IS THE WHOLE DEFENCE. A budget alone does not
|
|
64
|
+
* protect anything: one authenticated caller sending `MAX_MODEL_LABELS`
|
|
65
|
+
* requests naming random models — every one of them answered 404, because an
|
|
66
|
+
* unknown model is refused — would spend the entire budget and collapse every
|
|
67
|
+
* REAL model onto `(other)` until the pod restarts. That kills the per-model
|
|
68
|
+
* TTFT and rate panels, which are exactly the two the Hugging Face latency
|
|
69
|
+
* gate is read from. So a value is admitted only when `admit` is true, and the
|
|
70
|
+
* caller passes `admit` for answers that were actually served; a refusal can
|
|
71
|
+
* still be COUNTED (under `(other)`) but can never consume budget.
|
|
72
|
+
*/
|
|
73
|
+
declare class BoundedLabel {
|
|
74
|
+
private readonly max;
|
|
75
|
+
private readonly admitted;
|
|
76
|
+
private overflows;
|
|
77
|
+
constructor(max: number);
|
|
78
|
+
/**
|
|
79
|
+
* @param admit whether this observation may spend budget on a new value.
|
|
80
|
+
* False for refused requests, so a caller cannot blind the label by naming
|
|
81
|
+
* models that do not exist.
|
|
82
|
+
*/
|
|
83
|
+
of(value: string | null, admit?: boolean): string;
|
|
84
|
+
/** Distinct values admitted so far — published as a gauge (see below). */
|
|
85
|
+
get size(): number;
|
|
86
|
+
/** Admissible observations refused because the budget was spent. */
|
|
87
|
+
get overflowCount(): number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The hosted proxy's metric set, over its OWN {@link Registry} rather than the
|
|
91
|
+
* library's global `register`. Per-instance because the alternative is
|
|
92
|
+
* process-global mutable state that two tests (or two embedders) silently
|
|
93
|
+
* share — `Counter` construction on an already-registered name throws, so a
|
|
94
|
+
* global registry would make the second `createHostedProxy` in a process fail.
|
|
95
|
+
*/
|
|
96
|
+
declare class InferenceMetrics {
|
|
97
|
+
readonly registry: Registry<_prometheus_io_client.RegistryContentType>;
|
|
98
|
+
private readonly modelLabel;
|
|
99
|
+
private readonly orgLabel;
|
|
100
|
+
private readonly requests;
|
|
101
|
+
private readonly duration;
|
|
102
|
+
private readonly ttft;
|
|
103
|
+
private readonly streamErrors;
|
|
104
|
+
private readonly undelivered;
|
|
105
|
+
private readonly ledgerWrites;
|
|
106
|
+
private readonly ledgerQueue;
|
|
107
|
+
private readonly labelCardinality;
|
|
108
|
+
private readonly labelOverflows;
|
|
109
|
+
constructor(options?: {
|
|
110
|
+
defaultMetrics?: boolean;
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* Count ONE terminal ledger outcome — the third sink for a request that
|
|
114
|
+
* `proxy/ledger.ts` already logs, counted so it can be alerted on.
|
|
115
|
+
*
|
|
116
|
+
* Takes the very {@link LedgerOutcome} the log line carries, for the same
|
|
117
|
+
* reason {@link observe} takes the record verbatim: a counter derived from a
|
|
118
|
+
* second walk would eventually disagree with the billing log, and the
|
|
119
|
+
* disagreement would be invisible.
|
|
120
|
+
*/
|
|
121
|
+
observeLedger(outcome: LedgerOutcome): void;
|
|
122
|
+
/**
|
|
123
|
+
* Count ONE completed `/v1` request. Takes the very record
|
|
124
|
+
* `inferenceRequestLine` serializes, so the counters and the billing log can
|
|
125
|
+
* never drift apart.
|
|
126
|
+
*/
|
|
127
|
+
observe(record: InferenceRecord): void;
|
|
128
|
+
/** The exposition text a scrape answers with. */
|
|
129
|
+
scrape(): Promise<string>;
|
|
130
|
+
/** The `Content-Type` that exposition must be served under. */
|
|
131
|
+
get contentType(): string;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The PRIVATE metrics listener — its own `http.Server` on
|
|
135
|
+
* {@link METRICS_PORT}, serving exactly `GET /metrics` and 404 for everything
|
|
136
|
+
* else.
|
|
137
|
+
*
|
|
138
|
+
* A SEPARATE SERVER, not a route on the main one, is the whole point: the
|
|
139
|
+
* public gateway forwards only to the main port, so "not public" is a property
|
|
140
|
+
* of the socket rather than a rule the `/v1` router has to keep remembering.
|
|
141
|
+
* It also means a scrape can still be answered while the main server is
|
|
142
|
+
* draining, which is exactly when the numbers matter most.
|
|
143
|
+
*/
|
|
144
|
+
declare function createMetricsServer(metrics: InferenceMetrics): Server;
|
|
8
145
|
|
|
9
146
|
/**
|
|
10
147
|
* HOSTED AUTH — the Bearer key IS the identity AND the tenancy.
|
|
@@ -128,17 +265,24 @@ declare class OrgResolver {
|
|
|
128
265
|
* `consecutiveFailures` resets on every healthy sync, so a recovered
|
|
129
266
|
* blip never trips this.
|
|
130
267
|
*
|
|
131
|
-
*
|
|
132
|
-
* 'error')
|
|
133
|
-
*
|
|
268
|
+
* Core Session's own phase surface (`Session.onPhase` → 'live' /
|
|
269
|
+
* 'paused' / 'error'). The verdict arms ONLY on a `live` sighting: a
|
|
270
|
+
* binding must EXIST before it can be lost. A session born into its
|
|
271
|
+
* pre-live wait (the cold wake) or re-attached in `error` (resume runs
|
|
272
|
+
* the function from the top, session-semantics.md) is ACQUIRING
|
|
273
|
+
* compute, never losing it — core's own whenLive gate treats both as
|
|
274
|
+
* WAITABLE, and the unguarded latch 502-looped every shared chat/agent
|
|
275
|
+
* model on exactly those sightings (prod 2026-09-14: the entry was
|
|
276
|
+
* evicted before it ever served, and "retry to get a fresh session"
|
|
277
|
+
* re-dialed into the same verdict forever).
|
|
134
278
|
*
|
|
135
|
-
* NOTE what "gone" means HERE, because it is narrower than it reads:
|
|
136
|
-
* pool holds a session with COMPUTE BOUND to it, and a
|
|
137
|
-
* that binding is gone. The session ITSELF is not
|
|
138
|
-
* listed, and attachable by name (see
|
|
139
|
-
* urun-python/docs/design/session-semantics.md). The eviction is
|
|
140
|
-
* because the POOL ENTRY is what became unusable; nothing here
|
|
141
|
-
* to conclude the session is over.
|
|
279
|
+
* NOTE what "gone" means HERE, because it is narrower than it reads:
|
|
280
|
+
* this pool holds a session with COMPUTE BOUND to it, and a post-live
|
|
281
|
+
* `paused` phase says that binding is gone. The session ITSELF is not
|
|
282
|
+
* gone — it is parked, listed, and attachable by name (see
|
|
283
|
+
* urun-python/docs/design/session-semantics.md). The eviction is
|
|
284
|
+
* correct because the POOL ENTRY is what became unusable; nothing here
|
|
285
|
+
* is entitled to conclude the session is over.
|
|
142
286
|
*
|
|
143
287
|
* Consumers (cli.ts):
|
|
144
288
|
* - {@link watchSessionGone} per pooled entry; `onGone` → `router.evict`
|
|
@@ -220,6 +364,19 @@ type OwnedSession = UrunSessionLike & {
|
|
|
220
364
|
name: string;
|
|
221
365
|
reason?: string;
|
|
222
366
|
}) => void) => () => void;
|
|
367
|
+
whenLive?: (options?: {
|
|
368
|
+
timeout?: number;
|
|
369
|
+
signal?: AbortSignal;
|
|
370
|
+
}) => Promise<void>;
|
|
371
|
+
/**
|
|
372
|
+
* Core's NATIVE admission-ticket cancel (`Session.cancel`): hands back the
|
|
373
|
+
* claim a still-QUEUED call waits on — once the call HAS a session, the
|
|
374
|
+
* installed handler refuses loudly. Optional here because only the
|
|
375
|
+
* client-gone abandon path requires it; that path falls back to the native
|
|
376
|
+
* `detach` when a session object lacks it (the queued shape does not exist
|
|
377
|
+
* there), and never approximates with `end()`.
|
|
378
|
+
*/
|
|
379
|
+
cancel?: () => Promise<unknown>;
|
|
223
380
|
};
|
|
224
381
|
/**
|
|
225
382
|
* One pooled backhaul: the session, its (stateful) Responses client, and the
|
|
@@ -301,6 +458,24 @@ interface TenantRegistryOptions {
|
|
|
301
458
|
apiUrl: string;
|
|
302
459
|
/** The model-catalog oracle for routing rule 5, or null. */
|
|
303
460
|
catalog: CatalogConfig | null;
|
|
461
|
+
/**
|
|
462
|
+
* THE STATELESS DIRECT PATH (urun-ts #497): present ⇒ every tenant's
|
|
463
|
+
* clients carry a per-caller decision dial — discovery over the CALLER'S
|
|
464
|
+
* own key (`GET {apiUrl}/runtimes`), tokens minted with the caller's
|
|
465
|
+
* org/app/fn, the flat body POSTed to the ready runtime's `:8099/v1/decision`.
|
|
466
|
+
* `shared` (the shared org's API key, when mounted) extends the same dial
|
|
467
|
+
* to SHARED-catalog models: discovery under the shared key, the token's
|
|
468
|
+
* `tenant` stays the caller and `provider` names the pod org from the
|
|
469
|
+
* block's `shared_org_id` (the intake's provider-aware org gate). Absent
|
|
470
|
+
* ⇒ shared models keep the session dial. Absent ⇒ no dial exists and every
|
|
471
|
+
* decision rides the session lane byte-for-byte.
|
|
472
|
+
*/
|
|
473
|
+
decisionIntake?: {
|
|
474
|
+
secret: string;
|
|
475
|
+
shared?: {
|
|
476
|
+
apiKey: string;
|
|
477
|
+
};
|
|
478
|
+
};
|
|
304
479
|
/** Injected in tests; production builds real uRun backhauls. */
|
|
305
480
|
build?: (caller: CallerIdentity) => {
|
|
306
481
|
router: ModelRouter<PoolEntry>;
|
|
@@ -319,6 +494,24 @@ declare class TenantRegistry {
|
|
|
319
494
|
/** Insertion-ordered = LRU order, because a hit re-inserts at the end. */
|
|
320
495
|
private readonly tenants;
|
|
321
496
|
constructor(opts: TenantRegistryOptions);
|
|
497
|
+
/**
|
|
498
|
+
* THE NON-SECRET TENANCY LABEL, attached by the REGISTRY rather than by
|
|
499
|
+
* whichever builder produced the backhaul.
|
|
500
|
+
*
|
|
501
|
+
* The caller's org id is what the per-request record and the Prometheus
|
|
502
|
+
* series are broken down by (`proxy/responses-turn.ts` `ProxyClients.tenant`)
|
|
503
|
+
* — never `caller.apiKey`, which is the credential on this surface and would
|
|
504
|
+
* become a credential on every dashboard. Attaching it here means a backhaul
|
|
505
|
+
* can never come back unlabelled because a builder forgot.
|
|
506
|
+
*
|
|
507
|
+
* `Object.create`, not a mutation and not a spread: the source object keeps
|
|
508
|
+
* its identity and its prototype (so a class-based `ProxyClients` keeps its
|
|
509
|
+
* methods), and no other caller's backhaul is touched. Done ONCE per tenant,
|
|
510
|
+
* at build time — the returned object is then cached and reused for every
|
|
511
|
+
* later request from that key, which matters because the `/v1/responses`
|
|
512
|
+
* thread store is keyed on this very object identity.
|
|
513
|
+
*/
|
|
514
|
+
private label;
|
|
322
515
|
private build;
|
|
323
516
|
/** The backhaul for THIS caller, opened on first use and reused after. */
|
|
324
517
|
clientsFor(caller: CallerIdentity): ProxyClients;
|
|
@@ -443,6 +636,243 @@ declare class ConversationBackhauls {
|
|
|
443
636
|
detachAll(): Promise<void>;
|
|
444
637
|
}
|
|
445
638
|
|
|
639
|
+
/**
|
|
640
|
+
* THE HOSTED USAGE READ — the control-plane half of the canonical usage lane.
|
|
641
|
+
*
|
|
642
|
+
* The proxy holds no database credential and no standing platform secret
|
|
643
|
+
* (hosted/auth.ts). So the ledger read rides the CALLER'S OWN org API key to
|
|
644
|
+
* the control plane's `inference-usage` edge function — the same mechanism and
|
|
645
|
+
* the same credential `TenantRegistry` uses for `GET {apiUrl}/apps` — and the
|
|
646
|
+
* control plane derives the org from that key server-side and runs the
|
|
647
|
+
* org-scoped `urun_inference_usage_lookup` RPC.
|
|
648
|
+
*
|
|
649
|
+
* THAT IS WHY THIS LANE DOES NOT PRE-VERIFY THE KEY WITH `OrgResolver`. The
|
|
650
|
+
* surface that owns the data authenticates the key itself, so a second
|
|
651
|
+
* verification here would be a second source of truth about the same
|
|
652
|
+
* credential — and the org binding it produced would not be the one the read
|
|
653
|
+
* was actually scoped by. `bearerFrom` still runs at the call site, so a
|
|
654
|
+
* missing or malformed Authorization header is a loud 401 without a round trip.
|
|
655
|
+
*
|
|
656
|
+
* EVERY FAILURE IS LOUD. There is no shape of failure that answers with an
|
|
657
|
+
* empty `requests` list, because "we hold nothing for these ids" is precisely
|
|
658
|
+
* the answer that makes a billing caller stop asking — Hugging Face writes a
|
|
659
|
+
* request off ~30 minutes after it was served — so an outage that could wear
|
|
660
|
+
* that costume would be an invisible revenue hole.
|
|
661
|
+
*/
|
|
662
|
+
|
|
663
|
+
interface UsageClientOptions {
|
|
664
|
+
/** The org control-plane API base (`{apiUrl}`) the function is mounted under. */
|
|
665
|
+
apiUrl: string;
|
|
666
|
+
/** Injected in tests; production uses the platform fetch. */
|
|
667
|
+
fetchImpl?: typeof fetch;
|
|
668
|
+
/**
|
|
669
|
+
* Where a row this lane REFUSED TO ANSWER ABOUT is reported (ENG-416).
|
|
670
|
+
*
|
|
671
|
+
* A malformed ledger row is isolated rather than allowed to 502 the whole
|
|
672
|
+
* 10,000-id batch, and the row it dropped is then MISSING from the answer —
|
|
673
|
+
* which a billing caller cannot tell apart from an id we do not hold, and
|
|
674
|
+
* reads as "not priced yet" until the request is written off unbilled. So
|
|
675
|
+
* the drop must be an event somebody can alert on. Absent, it goes to this
|
|
676
|
+
* process's stdout (`parseUsageRecords`'s own default), never nowhere.
|
|
677
|
+
*
|
|
678
|
+
* It may be async — a sink that ships the line somewhere usually is — and a
|
|
679
|
+
* rejected promise from it is contained exactly as a synchronous throw is.
|
|
680
|
+
*/
|
|
681
|
+
onRejectedRow?: UsageRowReporter;
|
|
682
|
+
}
|
|
683
|
+
/** The batch ledger read, as one call per request. */
|
|
684
|
+
declare class UsageClient {
|
|
685
|
+
private readonly opts;
|
|
686
|
+
private readonly fetchImpl;
|
|
687
|
+
constructor(opts: UsageClientOptions);
|
|
688
|
+
/**
|
|
689
|
+
* Look up what we hold for these ids, as the holder of `apiKey`.
|
|
690
|
+
*
|
|
691
|
+
* Three outcomes, all explicit:
|
|
692
|
+
* - the control plane answers → validated {@link InferenceUsageRecord}s;
|
|
693
|
+
* - it rejects the key (401/403) → {@link ProxyAuthError} (401);
|
|
694
|
+
* - anything else → {@link UsageSurfaceError} (502).
|
|
695
|
+
*/
|
|
696
|
+
lookup(apiKey: string, inferenceIds: readonly string[]): Promise<InferenceUsageRecord[]>;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* THE HOSTED LEDGER WRITE — the control-plane half of the per-request ledger.
|
|
701
|
+
*
|
|
702
|
+
* The proxy holds no database credential and no standing platform secret
|
|
703
|
+
* (hosted/auth.ts: "NO STANDING CREDENTIAL... every upstream call it makes
|
|
704
|
+
* rides the CALLER'S OWN key"). So the ledger write rides the caller's own org
|
|
705
|
+
* API key to the control plane's `inference-ledger` function — the same
|
|
706
|
+
* mechanism and the same credential the ENG-318 read already uses — and the
|
|
707
|
+
* control plane derives the org AND the api_key_id from that key server-side
|
|
708
|
+
* before calling `urun_record_inference_request`.
|
|
709
|
+
*
|
|
710
|
+
* THAT PROPERTY IS SOUND ON THE READ AND INVERTED ON THE WRITE, and it is
|
|
711
|
+
* stated here because it is invisible in the code. On a read, riding the
|
|
712
|
+
* caller's key is precisely what enforces org scoping. On a WRITE TO A BILLING
|
|
713
|
+
* LEDGER it means the party being billed is the party authenticated to write
|
|
714
|
+
* the bill: a caller can read its `Inference-Id` off a streaming response head
|
|
715
|
+
* and race a fabricated row in ahead of this one. The control plane refuses
|
|
716
|
+
* the second write rather than upserting, so the attempt is LOUD instead of
|
|
717
|
+
* silent — see {@link LedgerDuplicateError}.
|
|
718
|
+
*
|
|
719
|
+
* ============================ THE ENG-410 RULING ============================
|
|
720
|
+
*
|
|
721
|
+
* That is a DETECTION, and it is the DELIBERATE, OWNER-APPROVED choice — not a
|
|
722
|
+
* gap somebody failed to close. The decision and the condition that reopens it
|
|
723
|
+
* are recorded here because the next person to read this file will otherwise
|
|
724
|
+
* re-derive the wrong answer from first principles.
|
|
725
|
+
*
|
|
726
|
+
* WHAT THE ATTACK ACTUALLY IS, bounded honestly. `org_id` and `api_key_id` are
|
|
727
|
+
* derived from the credential and the control-plane surface REFUSES both as
|
|
728
|
+
* body fields, so cross-tenant billing INJECTION is structurally impossible: a
|
|
729
|
+
* forged row can only ever land in the forger's OWN org. The only rational
|
|
730
|
+
* attack is therefore SELF-under-billing, it is not really a race (the header
|
|
731
|
+
* flushes at the head of a stream and this write happens on close, so the
|
|
732
|
+
* attacker has the whole generation), and it cannot be made quiet — every
|
|
733
|
+
* stolen request costs the attacker one refused write and one error line.
|
|
734
|
+
*
|
|
735
|
+
* REJECTED — GIVE THE PROXY A STANDING PLATFORM CREDENTIAL FOR THE WRITE. This
|
|
736
|
+
* is the obvious-sounding move and it is STRICTLY WORSE THAN THE FLAW. For a
|
|
737
|
+
* platform credential to REPLACE the caller's key on this write, `org_id` has
|
|
738
|
+
* to come back into the request body — there is nothing else left to carry
|
|
739
|
+
* tenancy. That deletes the one structural property that makes this surface
|
|
740
|
+
* safe at all, and it converts a proxy compromise from "whatever caller keys
|
|
741
|
+
* happen to be in flight" into "arbitrary billing rows for EVERY org, written
|
|
742
|
+
* from an internet-facing pod". The generalised rule, which outlives this
|
|
743
|
+
* module: A CREDENTIAL MAY BE ADDITIVE TO THE CALLER'S KEY, NEVER A
|
|
744
|
+
* REPLACEMENT FOR IT, because the caller's key is what carries tenancy.
|
|
745
|
+
*
|
|
746
|
+
* REJECTED — HAVE THE CONTROL PLANE DERIVE MORE AND TRUST THE BODY LESS. It
|
|
747
|
+
* already derives everything it can (`org_id`, `api_key_id`) and refuses what
|
|
748
|
+
* it must (`gpu_seconds`). Of what is left, the fields that DECIDE MONEY are
|
|
749
|
+
* exactly the fields it has no independent source for: nothing in the platform
|
|
750
|
+
* reports per-request token counts anywhere except this wire, and the
|
|
751
|
+
* `inference_id` is minted here. Authenticating the id would not help either —
|
|
752
|
+
* the attacker holds a LEGITIMATE id, read from its own response head.
|
|
753
|
+
*
|
|
754
|
+
* DEFERRED, WITH A TRIGGER — MAKE THE ROW UNFORGEABLE (a proxy-held key that
|
|
755
|
+
* signs the row's CONTENT, verified by the control plane, ADDITIVE to the
|
|
756
|
+
* caller's key so tenancy is untouched). This is the real fix and its blast
|
|
757
|
+
* radius is small: a stolen signing key only restores today's position,
|
|
758
|
+
* because the row still lands in whatever org the thief's own key names. It is
|
|
759
|
+
* NOT built yet because its own failure mode is worse than the flaw's while
|
|
760
|
+
* nothing bills off this table: a misconfigured or badly-rotated key takes
|
|
761
|
+
* 100% OF LEDGER ROWS TO ZERO ACROSS EVERY ORG, where the flaw takes some rows
|
|
762
|
+
* to zero for one customer attacking itself, loudly. Fitting a cryptographic
|
|
763
|
+
* gate to a writer that has never once run in production, in the week it first
|
|
764
|
+
* writes a row, is how a remedy costs more than the bug.
|
|
765
|
+
*
|
|
766
|
+
* THE TRIGGER, EXPLICITLY: BUILD IT BEFORE ANY PRODUCT BILLS OFF
|
|
767
|
+
* `public.inference_requests`. Today HF traffic authenticates as HF's own
|
|
768
|
+
* org (the end user never holds a uRun key) and self-serve is billed off
|
|
769
|
+
* `usage_events`, so no party both can run the attack and benefits from it.
|
|
770
|
+
* The credits / concurrency-tier / retention-tier product is the moment that
|
|
771
|
+
* stops being true, and it must not be the moment this is discovered.
|
|
772
|
+
*
|
|
773
|
+
* UNTIL THEN THE DETECTION IS THE CONTROL, so it is built to be OPERATED
|
|
774
|
+
* rather than merely to exist: the refusal carries a machine-readable
|
|
775
|
+
* `ledgerFailureReason` (`proxy/ledger.ts` {@link LedgerFailure}) that reaches
|
|
776
|
+
* both the structured log line and a Prometheus counter, so the alert is a
|
|
777
|
+
* field match and not a regex over this paragraph's prose.
|
|
778
|
+
*
|
|
779
|
+
* ===========================================================================
|
|
780
|
+
*
|
|
781
|
+
* EVERY FAILURE IS LOUD AND NONE OF THEM REACHES THE CUSTOMER. This runs from
|
|
782
|
+
* the response's `'close'` hook, after the answer is delivered, so there is no
|
|
783
|
+
* request left to fail: `proxy/ledger.ts` catches everything here and writes
|
|
784
|
+
* one structured log line. A write that did not land is revenue that was never
|
|
785
|
+
* recorded, which is exactly the thing that must never be silent.
|
|
786
|
+
*/
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* The control-plane route this writes. Relative to the org control-plane API
|
|
790
|
+
* base (`{apiUrl}`, i.e. `https://api.urun.sh/v1`), which the deployment's
|
|
791
|
+
* gateway rewrites onto the Supabase Edge Functions host — the same base and
|
|
792
|
+
* the same rewrite `GET {apiUrl}/apps` and the usage read already ride.
|
|
793
|
+
*/
|
|
794
|
+
declare const LEDGER_FUNCTION_PATH = "/inference-ledger";
|
|
795
|
+
/**
|
|
796
|
+
* How long one write may take before it is abandoned. A single-row insert plus
|
|
797
|
+
* two indexed price lookups is milliseconds; this bound exists so a stalled
|
|
798
|
+
* socket becomes a log line rather than a promise retained for the life of the
|
|
799
|
+
* pod. Deliberately TIGHTER than the usage read's 15s: that read answers a
|
|
800
|
+
* live poller that is waiting, while this write has nobody waiting on it and
|
|
801
|
+
* every second it holds is a slot in the in-flight bound.
|
|
802
|
+
*/
|
|
803
|
+
declare const LEDGER_WRITE_TIMEOUT_MS = 10000;
|
|
804
|
+
/**
|
|
805
|
+
* The row was already in the ledger. Its own type because it is not a fault
|
|
806
|
+
* and must not be logged as one: it means somebody wrote this `inference_id`
|
|
807
|
+
* before us, which is either a double-write defect in this proxy or a row
|
|
808
|
+
* fabricated by the org being billed. Nothing was written and nothing was
|
|
809
|
+
* overwritten.
|
|
810
|
+
*/
|
|
811
|
+
declare class LedgerDuplicateError extends Error implements LedgerFailure {
|
|
812
|
+
/**
|
|
813
|
+
* THE ENG-410 SIGNAL, as a field an alert can match.
|
|
814
|
+
*
|
|
815
|
+
* `duplicate` now means PROVABLY NOT OURS. Since the 409 carries the stored
|
|
816
|
+
* row (ENG-415), a conflict whose row matches what we are re-sending is our
|
|
817
|
+
* OWN earlier attempt and resolves as {@link LedgerAlreadyWrittenError}
|
|
818
|
+
* instead — so what is left here is a row somebody else wrote, or a row we
|
|
819
|
+
* could not read back to check. Both fail to the LOUD side on purpose.
|
|
820
|
+
*
|
|
821
|
+
* `duplicate_foreign` is the loudest state in the lane: the id is taken by a
|
|
822
|
+
* row THIS ORG CANNOT READ. `inference_requests.inference_id` is a global
|
|
823
|
+
* primary key, so it is reachable, and it can never be our own write.
|
|
824
|
+
*/
|
|
825
|
+
readonly ledgerFailureReason: 'duplicate' | 'duplicate_foreign';
|
|
826
|
+
constructor(message: string, reason?: 'duplicate' | 'duplicate_foreign');
|
|
827
|
+
}
|
|
828
|
+
/** The control-plane write surface could not be reached or refused the row. */
|
|
829
|
+
declare class LedgerSurfaceError extends Error implements LedgerFailure {
|
|
830
|
+
readonly ledgerFailureReason: "surface";
|
|
831
|
+
constructor(message: string);
|
|
832
|
+
}
|
|
833
|
+
interface LedgerClientOptions {
|
|
834
|
+
/** The org control-plane API base (`{apiUrl}`) the function is mounted under. */
|
|
835
|
+
apiUrl: string;
|
|
836
|
+
/** Injected in tests; production uses the platform fetch. */
|
|
837
|
+
fetchImpl?: typeof fetch;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Validate the control plane's answer into the record the caller logs.
|
|
841
|
+
*
|
|
842
|
+
* IT IS VALIDATED RATHER THAN TRUSTED because what it carries is a COST. A
|
|
843
|
+
* surface that changed shape under us must not be able to produce a log line
|
|
844
|
+
* claiming a request was priced at a number nobody returned — and
|
|
845
|
+
* `unpriced_reason` is what tells an operator that traffic is being served
|
|
846
|
+
* that nobody can bill, so a missing one is not a detail.
|
|
847
|
+
*/
|
|
848
|
+
declare function parseLedgerRecorded(raw: unknown, where: string): LedgerRecorded;
|
|
849
|
+
/** One ledger row, written as the holder of the caller's own org API key. */
|
|
850
|
+
declare class LedgerClient {
|
|
851
|
+
private readonly opts;
|
|
852
|
+
private readonly fetchImpl;
|
|
853
|
+
constructor(opts: LedgerClientOptions);
|
|
854
|
+
/**
|
|
855
|
+
* Write one row. Five outcomes, all explicit:
|
|
856
|
+
* - the control plane records it → the validated {@link LedgerRecorded},
|
|
857
|
+
* priced or with an `unpriced_reason`;
|
|
858
|
+
* - the id is already there AND the stored row is OURS (409) →
|
|
859
|
+
* {@link LedgerAlreadyWrittenError}, which is a SUCCESS: an earlier
|
|
860
|
+
* attempt landed;
|
|
861
|
+
* - the id is already there and the row is NOT ours, or cannot be read
|
|
862
|
+
* back to check (409) → {@link LedgerDuplicateError};
|
|
863
|
+
* - it rejects the key (401/403) → {@link LedgerAuthError};
|
|
864
|
+
* - anything else → {@link LedgerSurfaceError}.
|
|
865
|
+
*
|
|
866
|
+
* THE CALLER RETRIES, NOT THIS METHOD (ENG-415). One call is one attempt;
|
|
867
|
+
* `proxy/ledger.ts` owns the queue, the backoff and the dwell bound, and it
|
|
868
|
+
* retries only the one reason that is safe to retry. What makes any of it
|
|
869
|
+
* safe is the fifth outcome above: a retry whose first attempt secretly
|
|
870
|
+
* landed comes back as {@link LedgerAlreadyWrittenError}, not as something
|
|
871
|
+
* indistinguishable from a forged row.
|
|
872
|
+
*/
|
|
873
|
+
write(apiKey: string, entry: LedgerWrite): Promise<LedgerRecorded>;
|
|
874
|
+
}
|
|
875
|
+
|
|
446
876
|
/**
|
|
447
877
|
* The hosted endpoint's configuration.
|
|
448
878
|
*
|
|
@@ -491,6 +921,39 @@ interface HostedConfig {
|
|
|
491
921
|
apiUrl: string;
|
|
492
922
|
/** The `model_catalog` oracle for routing rule 5, or null when unconfigured. */
|
|
493
923
|
catalog: CatalogConfig | null;
|
|
924
|
+
/**
|
|
925
|
+
* THE STATELESS DIRECT PATH's credential (URUN_SESSION_TOKEN_SECRET) — the
|
|
926
|
+
* runtime scoped-token signing secret the decision intake verifies
|
|
927
|
+
* (urun-python decision_intake). A SECRET input, not a behavior knob: it is
|
|
928
|
+
* the mandate's allowed env class, and it is the SAME predicate the local
|
|
929
|
+
* lane enables on (cli.ts statelessDialEnabled), so the two lanes can never
|
|
930
|
+
* disagree. Absent ('') the stateless lane does not exist and every
|
|
931
|
+
* decision rides the session lane byte-for-byte. Present, decisions for
|
|
932
|
+
* CALLER-ORG serves="openai" apps dispatch straight to a ready runtime's
|
|
933
|
+
* `:8099/v1/decision`: discovery rides the CALLER'S OWN key
|
|
934
|
+
* (`GET {apiUrl}/runtimes`), and the per-decision token is minted with the
|
|
935
|
+
* caller's org/app/fn identity. Shared-catalog models keep the session
|
|
936
|
+
* dial — the v1 boundary #497 declares (the dial's own targetFor refuses
|
|
937
|
+
* them).
|
|
938
|
+
*
|
|
939
|
+
* OPTIONAL on this bag because legacy constructors may omit it
|
|
940
|
+
* (resolveHostedConfig always supplies it — '' when the env is absent);
|
|
941
|
+
* consumers read it as `?? ''`.
|
|
942
|
+
*/
|
|
943
|
+
decisionSecret?: string;
|
|
944
|
+
/**
|
|
945
|
+
* THE SHARED LANE'S credential (URUN_SHARED_ORG_API_KEY): the shared org's
|
|
946
|
+
* API key, so the decision dial can discover — and its tokens can name via
|
|
947
|
+
* the block's `shared_org_id` — the SHARED org's pods. Optional: absent ⇒
|
|
948
|
+
* shared models keep the session dial (the stateless boundary narrows to
|
|
949
|
+
* "no shared credential mounted"). URUN_SHARED_ORG_ID must ride WITH it
|
|
950
|
+
* (both-or-neither, loud) purely as the identity statement of the key —
|
|
951
|
+
* the dial never re-spells the pod org; it reads `shared_org_id` from the
|
|
952
|
+
* block that resolved the lane, so key and pod org cannot drift.
|
|
953
|
+
*/
|
|
954
|
+
sharedOrgKey?: string;
|
|
955
|
+
/** Identity statement accompanying `sharedOrgKey` (URUN_SHARED_ORG_ID). */
|
|
956
|
+
sharedOrgId?: string;
|
|
494
957
|
}
|
|
495
958
|
/**
|
|
496
959
|
* Read and VALIDATE the deployment's external inputs. Every failure is loud
|
|
@@ -520,11 +983,23 @@ declare function resolveHostedConfig(env?: NodeJS.ProcessEnv): HostedConfig;
|
|
|
520
983
|
* reachable, because a replica that cannot verify API keys
|
|
521
984
|
* or list apps can serve nothing and must be pulled out of
|
|
522
985
|
* the Service rather than answering 502s.
|
|
523
|
-
* * /v1/... the compat surface, org-scoped by the Bearer key
|
|
986
|
+
* * /v1/... the compat surface, org-scoped by the Bearer key, including
|
|
987
|
+
* the canonical usage-query lane POST /v1/usage/requests
|
|
988
|
+
* (proxy/usage.ts — a batch read of the per-request
|
|
989
|
+
* inference ledger).
|
|
990
|
+
* POST /partners/<name>/... the ENG-376 partner ADAPTER surface: a
|
|
991
|
+
* partner's own wire shape translated onto a canonical lane,
|
|
992
|
+
* sharing its auth and error mapping. Today: the Hugging
|
|
993
|
+
* Face billing poll. Ingress/charts must allowlist it.
|
|
524
994
|
* WS /ws/google.ai.generativelanguage.v1beta.GenerativeService.
|
|
525
995
|
* BidiGenerateContent — the Gemini Live surface, org-scoped by the
|
|
526
996
|
* Gemini credential (x-goog-api-key / Authorization: Bearer; `?key=`
|
|
527
997
|
* refused). Ingress/charts must allowlist that path.
|
|
998
|
+
* WS /v1/realtime — the OpenAI Realtime surface (GA protocol subset),
|
|
999
|
+
* org-scoped by the SAME Bearer api key as the /v1 lane; the
|
|
1000
|
+
* caller-org catalog's `task` column (stt | tts) gates which models
|
|
1001
|
+
* may bind an audio session. Ingress/charts must allowlist that
|
|
1002
|
+
* path too.
|
|
528
1003
|
* * anything else → 404 in the OpenAI error envelope (unclaimed WS
|
|
529
1004
|
* upgrades get their own final refusal).
|
|
530
1005
|
*
|
|
@@ -555,6 +1030,40 @@ interface HostedProxyOptions extends HostedConfig {
|
|
|
555
1030
|
registry?: TenantRegistry;
|
|
556
1031
|
/** Injected in tests so conversation backhauls are stubbed at the seam. */
|
|
557
1032
|
conversations?: ConversationBackhauls;
|
|
1033
|
+
/**
|
|
1034
|
+
* Injected in tests so the canonical usage lane is exercised without a
|
|
1035
|
+
* control plane. Production builds one against `apiUrl` — the same base and
|
|
1036
|
+
* the same caller-key credential `GET {apiUrl}/apps` rides.
|
|
1037
|
+
*/
|
|
1038
|
+
usageClient?: UsageClient;
|
|
1039
|
+
/**
|
|
1040
|
+
* Injected in tests so the per-request LEDGER WRITE is exercised without a
|
|
1041
|
+
* control plane. Production builds one against `apiUrl` — the same base and
|
|
1042
|
+
* the same caller-key credential the usage read and `GET {apiUrl}/apps`
|
|
1043
|
+
* ride.
|
|
1044
|
+
*/
|
|
1045
|
+
ledgerClient?: LedgerClient;
|
|
1046
|
+
/**
|
|
1047
|
+
* Sink for the ledger lane's own structured diagnostics (proxy/ledger.ts:
|
|
1048
|
+
* one JSON object per write, skip or failure). DELIBERATELY SEPARATE from
|
|
1049
|
+
* {@link requestLog}, which carries exactly one `inference_request` record
|
|
1050
|
+
* per `/v1` request and whose readers count on that. Production leaves it
|
|
1051
|
+
* unset, which is this container's stdout.
|
|
1052
|
+
*/
|
|
1053
|
+
ledgerLog?: (line: string) => void;
|
|
1054
|
+
/**
|
|
1055
|
+
* Sink for the shared handler's per-request billing log line (the
|
|
1056
|
+
* `Inference-Id` record — proxy/responses-turn.ts `ProxyHandlerOptions`).
|
|
1057
|
+
* Injected in tests; production leaves it unset, which is this container's
|
|
1058
|
+
* stdout — the log stream the platform collects.
|
|
1059
|
+
*/
|
|
1060
|
+
requestLog?: (line: string) => void;
|
|
1061
|
+
/**
|
|
1062
|
+
* Collect Node process metrics (event-loop lag, heap, GC) alongside the
|
|
1063
|
+
* request metrics. Production leaves it on; tests turn it off so an
|
|
1064
|
+
* assertion on the exposition text is not swamped by process noise.
|
|
1065
|
+
*/
|
|
1066
|
+
defaultMetrics?: boolean;
|
|
558
1067
|
}
|
|
559
1068
|
/**
|
|
560
1069
|
* Build (not listen) the hosted endpoint. The caller owns listen/close,
|
|
@@ -565,8 +1074,192 @@ interface HostedProxyOptions extends HostedConfig {
|
|
|
565
1074
|
*/
|
|
566
1075
|
declare function createHostedProxy(options: HostedProxyOptions): {
|
|
567
1076
|
server: Server;
|
|
1077
|
+
/**
|
|
1078
|
+
* The PRIVATE Prometheus listener (`proxy/metrics.ts` — GET /metrics on
|
|
1079
|
+
* {@link METRICS_PORT}). Its own server, not a route on the one above, so
|
|
1080
|
+
* "the metrics port is not public" is a property of the socket rather than
|
|
1081
|
+
* a rule the `/v1` router must keep remembering. The caller owns
|
|
1082
|
+
* listen/close, exactly as it does for `server`.
|
|
1083
|
+
*/
|
|
1084
|
+
metricsServer: Server;
|
|
1085
|
+
metrics: InferenceMetrics;
|
|
1086
|
+
/**
|
|
1087
|
+
* The PRIVATE ext-auth listener the edge calls to turn the caller's Bearer
|
|
1088
|
+
* key into a non-secret bucket id (`hosted/edge-identity.ts`). Its own
|
|
1089
|
+
* socket for the same reason the metrics port is: the public HTTPRoutes
|
|
1090
|
+
* forward only to the `/v1` port, so it cannot be reached from outside.
|
|
1091
|
+
* The caller owns listen/close.
|
|
1092
|
+
*/
|
|
1093
|
+
edgeIdentityServer: Server;
|
|
1094
|
+
/**
|
|
1095
|
+
* The tenant registry this proxy built (production wiring). Tests assert
|
|
1096
|
+
* the per-caller stateless dial through it; production code goes through
|
|
1097
|
+
* `callerOf`, never here.
|
|
1098
|
+
*/
|
|
1099
|
+
registry: TenantRegistry;
|
|
568
1100
|
closeAll: () => Promise<void>;
|
|
569
1101
|
closeWebSockets: () => Promise<void>;
|
|
570
1102
|
};
|
|
571
1103
|
|
|
572
|
-
|
|
1104
|
+
/**
|
|
1105
|
+
* THE EDGE IDENTITY SURFACE — the one thing Envoy cannot compute for itself.
|
|
1106
|
+
*
|
|
1107
|
+
* WHY IT EXISTS. Envoy Gateway's per-caller rate limiting needs a DISTINCT
|
|
1108
|
+
* bucket per caller, and the only caller identity on this endpoint is the
|
|
1109
|
+
* Bearer org API key (`hosted/auth.ts`: "the Bearer key IS the identity AND
|
|
1110
|
+
* the tenancy"). Envoy Gateway can key a global rate limit on a header's
|
|
1111
|
+
* distinct values — but the descriptor VALUE is what the rate-limit service
|
|
1112
|
+
* sends to Redis and what Redis stores as part of its key. Keying directly on
|
|
1113
|
+
* `Authorization` would therefore park live customer API keys, in plaintext,
|
|
1114
|
+
* in an in-cluster Redis whose NetworkPolicy is inert on both prod clusters
|
|
1115
|
+
* today (the VPC CNI node agent runs without `--enable-network-policy`, as the
|
|
1116
|
+
* valkey chart already documents). That is a credential store nobody designed,
|
|
1117
|
+
* audited, or rotates.
|
|
1118
|
+
*
|
|
1119
|
+
* Envoy's Lua filter has no hashing primitive and its rate-limit actions have
|
|
1120
|
+
* no transform, so SOMETHING has to turn the key into a non-secret id before
|
|
1121
|
+
* the rate-limit filter runs. Envoy Gateway's own mechanism for that is
|
|
1122
|
+
* `SecurityPolicy.extAuth`, whose response headers are merged into the request
|
|
1123
|
+
* ("coexisting headers will be overridden") before the later rate-limit filter
|
|
1124
|
+
* reads them. This module is that service.
|
|
1125
|
+
*
|
|
1126
|
+
* IT IS NOT AN AUTHORIZATION GATE, AND IT MUST NEVER BECOME ONE. It answers
|
|
1127
|
+
* 200 to everything. Authentication stays where it already is — in the proxy,
|
|
1128
|
+
* which alone can answer 401 in the lane's native error envelope with the
|
|
1129
|
+
* `Inference-Id` header HF bills on. A deny here would instead produce Envoy's
|
|
1130
|
+
* bodiless refusal, which an OpenAI SDK surfaces as an unparseable error.
|
|
1131
|
+
*
|
|
1132
|
+
* IT HAS NO DEPENDENCIES, ON PURPOSE. Read a header, hash it, answer. No
|
|
1133
|
+
* control-plane call, no cache, no I/O, nothing that can be slow or down. It
|
|
1134
|
+
* sits in the request path of `inference.urun.sh`, so the only acceptable
|
|
1135
|
+
* failure budget is none — and the `SecurityPolicy` that calls it is
|
|
1136
|
+
* additionally configured `failOpen`, so even losing it entirely degrades to
|
|
1137
|
+
* "no rate limiting", never to "no inference".
|
|
1138
|
+
*/
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* The container's edge-identity port — a MODULE CONSTANT, not an env var, per
|
|
1142
|
+
* the hosted config mandate (`hosted/config.ts`). Its own socket, like the
|
|
1143
|
+
* metrics port: private by construction, since the public HTTPRoutes forward
|
|
1144
|
+
* only to the `/v1` port.
|
|
1145
|
+
*/
|
|
1146
|
+
declare const EDGE_IDENTITY_PORT = 9465;
|
|
1147
|
+
/**
|
|
1148
|
+
* THE BUCKET ID. Envoy Gateway's `BackendTrafficPolicy` keys the caller's
|
|
1149
|
+
* rate-limit counter on this header's DISTINCT values, which means the value
|
|
1150
|
+
* here IS the input the rate-limit service builds its Redis counter key from.
|
|
1151
|
+
*
|
|
1152
|
+
* THAT IS WHY IT MUST NOT BE THE VALUE COMMITTED IN `values.yaml`. The tier
|
|
1153
|
+
* fingerprint below is written into a git-tracked manifest so an operator can
|
|
1154
|
+
* give one caller its own limit. If the bucket id were the same digest, then
|
|
1155
|
+
* ANYONE WHO CAN READ THE REPO could reconstruct a tiered caller's counter key
|
|
1156
|
+
* — and the counter store is reachable by any pod in the cluster with
|
|
1157
|
+
* `+incrby` and `+expire` granted, so they could spend Hugging Face's
|
|
1158
|
+
* allowance or stretch the window and hold HF's own probe at 429. That is the
|
|
1159
|
+
* delisting event this whole pair exists to prevent, and non-invertibility of
|
|
1160
|
+
* SHA-256 does nothing about it: the attacker never needs the key, only the
|
|
1161
|
+
* digest, and the digest was published on purpose.
|
|
1162
|
+
*
|
|
1163
|
+
* So the two digests are DIFFERENT one-way functions of the same token, split
|
|
1164
|
+
* by domain separator ({@link BUCKET_DOMAIN} vs {@link TIER_DOMAIN}). Knowing
|
|
1165
|
+
* the tier fingerprint gives no path to the bucket id: getting there would
|
|
1166
|
+
* require recovering the token from its digest, which is a preimage attack on
|
|
1167
|
+
* SHA-256 over 256 bits of `randomBytes` entropy.
|
|
1168
|
+
*/
|
|
1169
|
+
declare const EDGE_KEY_ID_HEADER = "x-urun-key-id";
|
|
1170
|
+
/**
|
|
1171
|
+
* THE TIER SELECTOR — matched with `Exact` / `RegularExpression` in the
|
|
1172
|
+
* BackendTrafficPolicy to decide WHICH limit applies, never to bucket.
|
|
1173
|
+
*
|
|
1174
|
+
* This is the digest an operator commits to `rateLimit.tiers[].keyIdSha256` in
|
|
1175
|
+
* k8s-manifests, so treat it as PUBLIC: everything about the design has to
|
|
1176
|
+
* hold when an attacker knows it.
|
|
1177
|
+
*
|
|
1178
|
+
* WHY A SECOND HEADER AT ALL. An Envoy Gateway rate-limit rule needs both
|
|
1179
|
+
* kinds of match on the caller at once: `Distinct` (give this caller their own
|
|
1180
|
+
* counter) AND an exact/inverted match (is this the partner key, or everyone
|
|
1181
|
+
* else). Whether a single selector may list the SAME header name under two
|
|
1182
|
+
* different match types is not something this repo can verify without a
|
|
1183
|
+
* cluster, and getting it wrong means Envoy Gateway rejects the policy —
|
|
1184
|
+
* which, on a fail-open limiter, is indistinguishable from "rate limiting
|
|
1185
|
+
* works" until somebody floods us. Two headers remove the question; carrying
|
|
1186
|
+
* two DIFFERENT digests is what makes publishing one of them safe.
|
|
1187
|
+
*/
|
|
1188
|
+
declare const EDGE_KEY_FINGERPRINT_HEADER = "x-urun-key-fingerprint";
|
|
1189
|
+
/**
|
|
1190
|
+
* Domain separator for the BUCKET digest. Never appears in a manifest, a log,
|
|
1191
|
+
* or a metric — the bucket id is derived here and read only by Envoy.
|
|
1192
|
+
*/
|
|
1193
|
+
declare const BUCKET_DOMAIN = "urun-ratelimit-bucket:";
|
|
1194
|
+
/**
|
|
1195
|
+
* Domain separator for the TIER digest — the one an operator reproduces from a
|
|
1196
|
+
* key with `printf 'urun-ratelimit-tier:%s' "$KEY" | sha256sum | cut -d' ' -f1`.
|
|
1197
|
+
*
|
|
1198
|
+
* A plain `sha256sum "$KEY"` would be the OLD, unsafe value: identical to the
|
|
1199
|
+
* bucket id, and therefore a published counter key. The separator is what
|
|
1200
|
+
* keeps the two apart, so it is part of the operator contract and cannot drift.
|
|
1201
|
+
*/
|
|
1202
|
+
declare const TIER_DOMAIN = "urun-ratelimit-tier:";
|
|
1203
|
+
/**
|
|
1204
|
+
* The bucket every request WITHOUT a Bearer token shares. A single shared
|
|
1205
|
+
* bucket rather than no bucket at all: unauthenticated floods must be limited
|
|
1206
|
+
* too, and they have no identity to spread across. They cannot consume a real
|
|
1207
|
+
* caller's bucket because no real key hashes to this value.
|
|
1208
|
+
*/
|
|
1209
|
+
declare const ANONYMOUS_KEY_ID = "anonymous";
|
|
1210
|
+
/**
|
|
1211
|
+
* The caller's BUCKET id — `sha256(BUCKET_DOMAIN || token)`, hex.
|
|
1212
|
+
*
|
|
1213
|
+
* SECRET BY CONSTRUCTION, not because the digest is one-way but because the
|
|
1214
|
+
* only published digest is a DIFFERENT one. This value is the input Envoy's
|
|
1215
|
+
* rate-limit descriptor carries, so it is what the counter key in the
|
|
1216
|
+
* rate-limit cache is built from; publishing it would publish a writable
|
|
1217
|
+
* counter key (see {@link EDGE_KEY_ID_HEADER}).
|
|
1218
|
+
*/
|
|
1219
|
+
declare function edgeBucketId(bearerToken: string): string;
|
|
1220
|
+
/**
|
|
1221
|
+
* The caller's TIER fingerprint — `sha256(TIER_DOMAIN || token)`, hex.
|
|
1222
|
+
*
|
|
1223
|
+
* PUBLIC BY DESIGN: this is the value an operator commits to
|
|
1224
|
+
* `rateLimit.tiers[].keyIdSha256`. The runbook is
|
|
1225
|
+
* `printf 'urun-ratelimit-tier:%s' "$KEY" | sha256sum | cut -d' ' -f1`.
|
|
1226
|
+
*
|
|
1227
|
+
* The domain prefix is not decoration — a bare `sha256sum "$KEY"` reproduces
|
|
1228
|
+
* the BUCKET id, which is exactly the value that must never be committed. The
|
|
1229
|
+
* `cut` is load-bearing too: `sha256sum` prints the digest, two spaces, then
|
|
1230
|
+
* the input name, and a tier carrying that trailing ` -` matches nothing and
|
|
1231
|
+
* silently leaves the caller on the default limit.
|
|
1232
|
+
*/
|
|
1233
|
+
declare function edgeTierFingerprint(bearerToken: string): string;
|
|
1234
|
+
/**
|
|
1235
|
+
* The bearer token of an `Authorization` header, or null.
|
|
1236
|
+
*
|
|
1237
|
+
* Deliberately NOT `hosted/auth.ts` `bearerFrom`: that one THROWS a 401-shaped
|
|
1238
|
+
* error for a missing or malformed header, which is the correct behaviour for
|
|
1239
|
+
* the lane that authenticates. Here a malformed header must produce the
|
|
1240
|
+
* anonymous bucket and a 200, because refusing is the proxy's job and this
|
|
1241
|
+
* service must never refuse.
|
|
1242
|
+
*/
|
|
1243
|
+
declare function bearerTokenOf(headerValue: string | undefined): string | null;
|
|
1244
|
+
/**
|
|
1245
|
+
* The PRIVATE ext-auth listener. Answers 200 to every method and every path —
|
|
1246
|
+
* Envoy appends the ORIGINAL request path to the configured ext-auth path, so
|
|
1247
|
+
* this service sees `/ratelimit-identity/v1/chat/completions` and friends and
|
|
1248
|
+
* must not care.
|
|
1249
|
+
*
|
|
1250
|
+
* The response carries exactly the two identity headers below, and the
|
|
1251
|
+
* `SecurityPolicy` forwards exactly those two (`headersToBackend`). Nothing
|
|
1252
|
+
* about the caller's request, and certainly not their key, is echoed.
|
|
1253
|
+
*
|
|
1254
|
+
* A CALLER CANNOT PRESENT THEIR OWN. `headersToBackend` is documented as
|
|
1255
|
+
* overriding coexisting headers, so whatever a client sent under these names is
|
|
1256
|
+
* replaced on every request this service answers. There is deliberately NO
|
|
1257
|
+
* `ClientTrafficPolicy` stripping them at the listener: that policy would
|
|
1258
|
+
* attach to the shared `public-gw` (api.urun.sh rides it too) to close a gap
|
|
1259
|
+
* that grants nothing — in the only case where a client value survives, the
|
|
1260
|
+
* ext-auth hop has already failed open, and fail-open grants MORE than any
|
|
1261
|
+
* spoofed bucket could. The k8s half records the same decision.
|
|
1262
|
+
*/
|
|
1263
|
+
declare function createEdgeIdentityServer(): Server;
|
|
1264
|
+
|
|
1265
|
+
export { ANONYMOUS_KEY_ID, BIND_HOST, BUCKET_DOMAIN, BoundedLabel, type CallerIdentity, ControlPlaneUnavailableError, type ConversationBackhaulOptions, ConversationBackhauls, ConversationCapacityError, DEFAULT_PORT, EDGE_IDENTITY_PORT, EDGE_KEY_FINGERPRINT_HEADER, EDGE_KEY_ID_HEADER, type HostedConfig, type HostedProxyOptions, InferenceMetrics, LEDGER_FUNCTION_PATH, LEDGER_WRITE_TIMEOUT_MS, LedgerClient, type LedgerClientOptions, LedgerDuplicateError, LedgerSurfaceError, MAX_CACHED_KEYS, MAX_CONVERSATIONS, MAX_CONVERSATION_HANDLES, MAX_MODEL_LABELS, MAX_ORG_LABELS, MAX_TENANTS, METRICS_PATH, METRICS_PORT, NO_MODEL_LABEL, ORG_BINDING_TTL_MS, OTHER_LABEL, OrgResolver, type OrgResolverOptions, type PrivateConversationBackhaul, type PrivateConversationConnection, ProxyAuthError, READINESS_TIMEOUT_MS, READINESS_TTL_MS, SERVE_FUNCTION, TIER_DOMAIN, TenantRegistry, type TenantRegistryOptions, bearerFrom, bearerTokenOf, createEdgeIdentityServer, createHostedProxy, createMetricsServer, edgeBucketId, edgeTierFingerprint, laneOf, parseLedgerRecorded, resolveHostedConfig, statusClassOf, tenantSubject };
|