@urun-sh/openai 0.2.60 → 0.3.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/chunk-35LB5OHI.js +1 -0
- package/dist/chunk-3ED25N7E.js +1 -0
- package/dist/chunk-EFBD3CGZ.js +1 -0
- package/dist/chunk-F2TEK34X.js +1 -0
- package/dist/chunk-VG3X2LVG.js +41 -0
- package/dist/chunk-XHIIEA6Z.js +1 -0
- package/dist/chunk-XHRPJIEK.js +6 -0
- package/dist/chunk-YSFSRI3D.js +1 -0
- package/dist/hosted/bin.cjs +56 -0
- package/dist/hosted/bin.js +23 -0
- package/dist/hosted/index.cjs +34 -0
- package/dist/hosted/index.d.cts +620 -0
- package/dist/hosted/index.d.ts +225 -0
- package/dist/hosted/index.js +1 -0
- package/dist/index.js +1 -1
- package/dist/pi-extension/index.cjs +4 -4
- package/dist/pi-extension/index.js +7 -7
- package/dist/pi-extension/standalone.cjs +53 -53
- package/dist/proxy/cli.cjs +34 -34
- package/dist/proxy/cli.js +14 -19
- package/dist/proxy/index.cjs +21 -21
- package/dist/proxy/index.d.cts +4 -154
- package/dist/proxy/index.d.ts +4 -64
- package/dist/proxy/index.js +1 -1
- package/dist/server-BBKNv7hV.d.ts +78 -0
- package/dist/server-DiQUIDDg.d.cts +182 -0
- package/package.json +12 -2
- package/dist/chunk-4FUBGLFN.js +0 -41
- package/dist/chunk-OLE2YJO3.js +0 -1
- package/dist/chunk-SSZL77P5.js +0 -1
- package/dist/chunk-VFHQZ4OM.js +0 -1
- package/dist/chunk-VXTNG2TP.js +0 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { createClientToken } from '@urun-sh/core';
|
|
3
|
+
import { a as UrunSessionLike, U as UrunResponses } from '../ResponsesClient-y8g6OfNN.cjs';
|
|
4
|
+
import { P as ProxyClients } from '../server-DiQUIDDg.cjs';
|
|
5
|
+
import '../media-DCHTX3Ez.cjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-model → per-app routing for the compat proxy (owner directive
|
|
9
|
+
* 2026-08-07): swapping the model in a coding harness routes the request to
|
|
10
|
+
* the org's DEPLOYED app for that model. v1 is deployed-only — a uRun model
|
|
11
|
+
* that is not deployed gets a loud 404 naming `urun serve <id>`; a later
|
|
12
|
+
* phase (explicitly out of scope here; urun-infra#1490 shared-endpoints)
|
|
13
|
+
* auto-creates from the model catalog on first request.
|
|
14
|
+
*
|
|
15
|
+
* MODEL-ID SURFACE (the documented mapping): a model may be named by
|
|
16
|
+
* - the app slug itself ("qwen3-6-27b-bf16"), or
|
|
17
|
+
* - the catalog id ("qwen3.6-27b"), or
|
|
18
|
+
* - the catalog id:variant ("qwen3.6-27b:bf16"),
|
|
19
|
+
* where slugification mirrors urun-cli `serve.py _default_app_name` exactly:
|
|
20
|
+
* lowercase, every non-alphanumeric-non-dash character becomes "-", leading/
|
|
21
|
+
* trailing dashes stripped (catalog id "qwen3.6-27b" + variant "bf16" → app
|
|
22
|
+
* "qwen3-6-27b-bf16"). COLLISION RULE: an exact slug match always wins over
|
|
23
|
+
* the catalog-id (prefix) interpretation.
|
|
24
|
+
*
|
|
25
|
+
* RESOLUTION ORDER (one canonical path, documented end to end):
|
|
26
|
+
* 1. model absent / "urun" / an alias of the startup app → DEFAULT app.
|
|
27
|
+
* 2. exact slug match on a deployed serve app → that app.
|
|
28
|
+
* 3. catalog-id form matching exactly one deployed app → that app
|
|
29
|
+
* (two or more candidates → loud ambiguity error naming them).
|
|
30
|
+
* 4. the name maps to an org app that is NOT an active app exposing the
|
|
31
|
+
* proxy's serve function → loud 404
|
|
32
|
+
* naming `urun serve <model>` and the available models.
|
|
33
|
+
* 5. the name matches a catalog row but no deployed app → loud 404
|
|
34
|
+
* naming `urun serve <id>` (deployed-only v1).
|
|
35
|
+
* 6. anything else — a model name outside the uRun namespace entirely
|
|
36
|
+
* (e.g. the harness's own upstream default, "claude-*"/"gpt-*") →
|
|
37
|
+
* DEFAULT app. This IS today's single-app contract, kept deliberately
|
|
38
|
+
* so `urun compat <agent>` with the agent's stock model keeps working
|
|
39
|
+
* with zero new env; the per-model /stats table records every such
|
|
40
|
+
* mapping so it is visible, never silent. Models the proxy ADVERTISES
|
|
41
|
+
* on /v1/models can never land here — they resolve (2/3) or fail loud
|
|
42
|
+
* (4/5) above.
|
|
43
|
+
*
|
|
44
|
+
* NO-DEFAULT MODE (`defaultApp: null`) — the HOSTED multi-tenant lane
|
|
45
|
+
* (`src/hosted/`): a shared endpoint serving every org has no "the app this
|
|
46
|
+
* proxy was started for", so rules 1 and 6 have nothing to fall back TO.
|
|
47
|
+
* Rather than inventing one (picking "some" app for a caller would be the
|
|
48
|
+
* worst kind of silent divergence), both rules become the SAME loud
|
|
49
|
+
* {@link UnknownModelError} that rules 4/5 already raise: name a deployed
|
|
50
|
+
* model, here is the list. Rules 2–5 are byte-for-byte the local behavior —
|
|
51
|
+
* one router, one resolution order, two configurations.
|
|
52
|
+
*/
|
|
53
|
+
/** One org app row from `GET {orgApi}/apps` (urun-cli `ApiClient.list_apps`). */
|
|
54
|
+
interface DeployedApp {
|
|
55
|
+
app_slug: string;
|
|
56
|
+
function_name?: string | null;
|
|
57
|
+
deployment_status?: string | null;
|
|
58
|
+
[k: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A session handle names a pooled session that no longer exists (closed,
|
|
62
|
+
* evicted after its pod died, replaced by a re-home, or the proxy restarted).
|
|
63
|
+
* The caller asked to REATTACH that exact session — opening a fresh one and
|
|
64
|
+
* calling it "resumed" would be a silent lie, so this is always loud.
|
|
65
|
+
*/
|
|
66
|
+
declare class SessionGoneError extends Error {
|
|
67
|
+
}
|
|
68
|
+
/** OpenAI-shaped model list (same shape as models.ts listModels). */
|
|
69
|
+
interface RouterModelList {
|
|
70
|
+
object: 'list';
|
|
71
|
+
data: Array<{
|
|
72
|
+
id: string;
|
|
73
|
+
object: 'model';
|
|
74
|
+
created: number;
|
|
75
|
+
owned_by: string;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
interface ModelRouterOptions<S> {
|
|
79
|
+
/**
|
|
80
|
+
* The startup app slug (URUN_APP) — the DEFAULT model — or `null` for the
|
|
81
|
+
* hosted multi-tenant lane, which has no per-proxy default app: there,
|
|
82
|
+
* every request must NAME a deployed model and an unnamed/unknown one
|
|
83
|
+
* fails loud instead of silently landing somewhere (see the module header,
|
|
84
|
+
* "NO-DEFAULT MODE").
|
|
85
|
+
*/
|
|
86
|
+
defaultApp: string | null;
|
|
87
|
+
/** The serve function name every routed app must expose (URUN_FUNCTION). */
|
|
88
|
+
fnName: string;
|
|
89
|
+
/** Open a backhaul session for an app slug (called at most once per app). */
|
|
90
|
+
openSession: (appSlug: string) => S | Promise<S>;
|
|
91
|
+
/** Terminal release for one pool entry (Session.end() underneath). */
|
|
92
|
+
closeSession: (entry: S) => Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* List the org's deployed apps, or null when the credentials cannot
|
|
95
|
+
* (URUN_JWT lane: the pre-vended token is scoped to the default app, so
|
|
96
|
+
* there is no org listing AND no cross-app session — routing degrades to
|
|
97
|
+
* the default-app-only contract, which is exactly today's behavior).
|
|
98
|
+
*/
|
|
99
|
+
listApps: (() => Promise<DeployedApp[]>) | null;
|
|
100
|
+
/**
|
|
101
|
+
* Catalog rows (models.ts fetchCatalogRows) as the uRun-namespace oracle
|
|
102
|
+
* for rule 5, or null when catalog access is not configured.
|
|
103
|
+
*/
|
|
104
|
+
listCatalog: (() => Promise<Array<{
|
|
105
|
+
model_id: string;
|
|
106
|
+
variant: string;
|
|
107
|
+
}>>) | null;
|
|
108
|
+
/** Deployed-apps cache TTL (the list changes on deploys, not per request). */
|
|
109
|
+
appsTtlMs?: number;
|
|
110
|
+
/**
|
|
111
|
+
* The stable NATIVE identity of one pooled entry (the uRun session id in
|
|
112
|
+
* the proxy wiring — the same identity the serve-side session-affinity tag
|
|
113
|
+
* rides, urun-python#1556). Powers the session-identity seam
|
|
114
|
+
* ({@link ModelRouter.handleFor} / {@link ModelRouter.sessionForHandle});
|
|
115
|
+
* a router without it fails LOUD on those calls, never approximates.
|
|
116
|
+
*/
|
|
117
|
+
sessionKey?: (entry: S) => string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The session pool: one backhaul session per deployed app, keyed by app slug,
|
|
121
|
+
* opened lazily on the first request that routes to it and reused for every
|
|
122
|
+
* subsequent one. The startup app is seeded eagerly by the CLI. Sessions
|
|
123
|
+
* close on proxy shutdown via {@link closeAll}; there is NO idle-close policy
|
|
124
|
+
* (deliberate v1 simplification — noted as a follow-up in the PR).
|
|
125
|
+
*/
|
|
126
|
+
declare class ModelRouter<S> {
|
|
127
|
+
private readonly opts;
|
|
128
|
+
private readonly pool;
|
|
129
|
+
private appsCache;
|
|
130
|
+
constructor(opts: ModelRouterOptions<S>);
|
|
131
|
+
/** Seed an already-open session (the CLI's eagerly-opened startup app). */
|
|
132
|
+
seed(appSlug: string, entry: S): void;
|
|
133
|
+
private deployedApps;
|
|
134
|
+
/** Apps this proxy may serve: active AND exposing the serve function. */
|
|
135
|
+
private servable;
|
|
136
|
+
private availableIds;
|
|
137
|
+
/**
|
|
138
|
+
* NO-DEFAULT MODE's terminal for rules 1 and 6: there is no app to fall
|
|
139
|
+
* back to, so say so loudly and list what the CALLER'S org actually has.
|
|
140
|
+
* Never returns.
|
|
141
|
+
*/
|
|
142
|
+
private noDefaultApp;
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a request's `model` to an app slug — the documented resolution
|
|
145
|
+
* order from the module header. Throws {@link UnknownModelError} for a uRun
|
|
146
|
+
* model that is not deployed (rules 4/5).
|
|
147
|
+
*/
|
|
148
|
+
resolveApp(model: string | undefined): Promise<string>;
|
|
149
|
+
/** The pooled session for a model — opened lazily, reused afterwards. */
|
|
150
|
+
sessionFor(model: string | undefined): Promise<{
|
|
151
|
+
app: string;
|
|
152
|
+
entry: S;
|
|
153
|
+
}>;
|
|
154
|
+
private keyOf;
|
|
155
|
+
/**
|
|
156
|
+
* SESSION-IDENTITY SEAM (a): the opaque stable handle for the pooled
|
|
157
|
+
* session currently serving `model`'s turns. Rides the SAME acquisition
|
|
158
|
+
* path as every request ({@link sessionFor}) — the session opens lazily if
|
|
159
|
+
* this model has none yet — and derives the handle from native identity
|
|
160
|
+
* (app slug + uRun session id), zero bespoke bookkeeping.
|
|
161
|
+
*/
|
|
162
|
+
handleFor(model: string | undefined): Promise<{
|
|
163
|
+
app: string;
|
|
164
|
+
handle: string;
|
|
165
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* SESSION-IDENTITY SEAM (b): the exact pooled session a handle names.
|
|
168
|
+
* NEVER opens a fresh session — a handle whose session is gone (closed,
|
|
169
|
+
* evicted, re-homed to a replacement, proxy restarted) or malformed throws
|
|
170
|
+
* {@link SessionGoneError} loudly. Resume is reattach-or-fail, not
|
|
171
|
+
* reattach-or-quietly-restart.
|
|
172
|
+
*/
|
|
173
|
+
sessionForHandle(handle: string): Promise<{
|
|
174
|
+
app: string;
|
|
175
|
+
entry: S;
|
|
176
|
+
}>;
|
|
177
|
+
/**
|
|
178
|
+
* Drop ONE pooled session whose backhaul died (its pod was restarted /
|
|
179
|
+
* drained / deleted) and release it — the next {@link sessionFor} opens a
|
|
180
|
+
* fresh one, i.e. asks the control plane for a new assignment. Used by the
|
|
181
|
+
* one-shot re-home (rehome.ts, urun-sh/urun-python#1592).
|
|
182
|
+
*
|
|
183
|
+
* IDENTITY-GUARDED (the same rule the pi lane's SessionPool follows): a
|
|
184
|
+
* concurrent request that already re-homed this app has put a NEWER entry
|
|
185
|
+
* under the key, and evicting that would close a healthy session out from
|
|
186
|
+
* under it.
|
|
187
|
+
*/
|
|
188
|
+
evict(app: string, entry: S): Promise<void>;
|
|
189
|
+
/**
|
|
190
|
+
* `GET /v1/models`: the org's deployed serve apps as model entries, the
|
|
191
|
+
* default app FIRST. On the JWT lane (no org listing) this is the default
|
|
192
|
+
* app plus any app already in the pool — the gap is called out loudly in
|
|
193
|
+
* the PR, not papered over here.
|
|
194
|
+
*/
|
|
195
|
+
modelList(): Promise<RouterModelList>;
|
|
196
|
+
/** Close every pooled session (Session.end() underneath) — proxy shutdown. */
|
|
197
|
+
closeAll(): Promise<void>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* HOSTED AUTH — the Bearer key IS the identity AND the tenancy.
|
|
202
|
+
*
|
|
203
|
+
* Owner directive (2026-08-19): "the Bearer key alone determines the org —
|
|
204
|
+
* resolve org_id from the key server-side and scope ALL routing to THAT org.
|
|
205
|
+
* No org id in the URL or headers." So this module has exactly one job:
|
|
206
|
+
* `Authorization: Bearer <urun org api key>` → `org_id`, or a loud 401.
|
|
207
|
+
*
|
|
208
|
+
* IT INVENTS NO AUTH SCHEME. It calls the platform's OWN key verification —
|
|
209
|
+
* `createClientToken(apiKey)` from `@urun-sh/core`, i.e.
|
|
210
|
+
* `POST {gateway}/api/client-tokens` with the key on `Authorization` — which
|
|
211
|
+
* is the same call `urun compat` already makes to open a session, and which
|
|
212
|
+
* returns `org_id`: the key's org binding, straight from the control plane.
|
|
213
|
+
* There is no JWT path, no org header, no locally-held mapping table.
|
|
214
|
+
*
|
|
215
|
+
* NO STANDING CREDENTIAL. This process holds no platform secret at all: every
|
|
216
|
+
* upstream call it makes rides the CALLER'S OWN key. That is what makes an
|
|
217
|
+
* internet-facing shared endpoint safe to run, and it is what makes cross-org
|
|
218
|
+
* isolation structural rather than a rule this code has to remember — a key
|
|
219
|
+
* simply cannot mint a token, list apps, or dial a session outside its org.
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A refused request. `status` is what the caller sees; the message is the
|
|
224
|
+
* OpenAI-envelope `error.message`. Every rejection here is explicit — there
|
|
225
|
+
* is no anonymous lane to fall through to.
|
|
226
|
+
*/
|
|
227
|
+
declare class ProxyAuthError extends Error {
|
|
228
|
+
readonly status: number;
|
|
229
|
+
constructor(message: string, status?: number);
|
|
230
|
+
}
|
|
231
|
+
/** The control plane could not be reached/asked. Loud 502, never a pass. */
|
|
232
|
+
declare class ControlPlaneUnavailableError extends Error {
|
|
233
|
+
constructor(message: string);
|
|
234
|
+
}
|
|
235
|
+
/** The verified caller: their key, and the org the control plane bound it to. */
|
|
236
|
+
interface CallerIdentity {
|
|
237
|
+
apiKey: string;
|
|
238
|
+
orgId: string;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Pull the raw key off `Authorization: Bearer <key>`. Missing, malformed, or
|
|
242
|
+
* a non-Bearer scheme is a 401 — never an anonymous request.
|
|
243
|
+
*/
|
|
244
|
+
declare function bearerFrom(headerValue: string | undefined): string;
|
|
245
|
+
/**
|
|
246
|
+
* How long a verified key→org binding is reused before re-asking the control
|
|
247
|
+
* plane. Bounded and short: a revoked key stops working within this window,
|
|
248
|
+
* and until then it can still only reach its OWN org (the binding is the org
|
|
249
|
+
* it always had), so the window can never widen a tenancy boundary.
|
|
250
|
+
*/
|
|
251
|
+
declare const ORG_BINDING_TTL_MS = 60000;
|
|
252
|
+
/** The most distinct keys one replica remembers verifying (bounded memory). */
|
|
253
|
+
declare const MAX_CACHED_KEYS = 4096;
|
|
254
|
+
interface OrgResolverOptions {
|
|
255
|
+
/** The session-gateway base the mint call goes to (external input). */
|
|
256
|
+
gatewayUrl: string;
|
|
257
|
+
/** Injected in tests; production uses the SDK's own fetch. */
|
|
258
|
+
mint?: typeof createClientToken;
|
|
259
|
+
/** Injected in tests so cache expiry is exercised without wall-clock waits. */
|
|
260
|
+
now?: () => number;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Key → org resolution with a short bounded cache.
|
|
264
|
+
*
|
|
265
|
+
* The cache is keyed on the RAW key held only in memory for the TTL. It is
|
|
266
|
+
* deliberately not a "session registry": it holds no session, no doc, no
|
|
267
|
+
* routing state — nothing that would have to be shared between replicas, and
|
|
268
|
+
* nothing whose loss changes behavior (a cold replica just re-verifies).
|
|
269
|
+
*/
|
|
270
|
+
declare class OrgResolver {
|
|
271
|
+
private readonly opts;
|
|
272
|
+
private readonly cache;
|
|
273
|
+
private readonly mint;
|
|
274
|
+
private readonly now;
|
|
275
|
+
constructor(opts: OrgResolverOptions);
|
|
276
|
+
/**
|
|
277
|
+
* Verify a Bearer key against the control plane and return its org binding.
|
|
278
|
+
*
|
|
279
|
+
* Three outcomes, all explicit:
|
|
280
|
+
* - the control plane binds the key to an org → {@link CallerIdentity};
|
|
281
|
+
* - the control plane rejects the key (401/403) → {@link ProxyAuthError};
|
|
282
|
+
* - the control plane cannot be asked → loud
|
|
283
|
+
* {@link ControlPlaneUnavailableError} (502). NEVER a pass: a proxy that
|
|
284
|
+
* admitted requests while it could not verify keys would be an open
|
|
285
|
+
* relay into every org.
|
|
286
|
+
*/
|
|
287
|
+
resolve(apiKey: string): Promise<CallerIdentity>;
|
|
288
|
+
/** Test/ops seam: forget every cached binding. */
|
|
289
|
+
clear(): void;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* SESSION GONE for the proxy serve lane — level-triggered, keyed off the
|
|
294
|
+
* transport's OWN close/rejection signals (live defect, prod-usw2 2026-08-12).
|
|
295
|
+
*
|
|
296
|
+
* A pooled backhaul session can be closed by the PLATFORM out from under the
|
|
297
|
+
* proxy (close_reason=idle_timeout, or a serve-side crash close). The request
|
|
298
|
+
* marker is a write into the session's `llm` Yjs doc — and a Yjs provider
|
|
299
|
+
* whose room is gone buffers the write locally and redials FOREVER (that is
|
|
300
|
+
* its contract: "Still retrying — the doc provider never gives up"), so every
|
|
301
|
+
* request dispatched into a dead session was silently swallowed: zero events,
|
|
302
|
+
* no error, no re-home (live receipt: session
|
|
303
|
+
* ef1bf00c-24d7-4327-b057-452a72221e43 idle-closed 18:50:55Z; a /v1/messages
|
|
304
|
+
* request accepted 18:51:18Z produced nothing for 147s until the client gave
|
|
305
|
+
* up).
|
|
306
|
+
*
|
|
307
|
+
* This module derives ONE latched "session gone" verdict from the two native
|
|
308
|
+
* lifecycle surfaces core already exposes — NO liveness timers, NO
|
|
309
|
+
* heartbeats, no polling (the pi lane's stall watchdog stays a pi-lane
|
|
310
|
+
* concern):
|
|
311
|
+
*
|
|
312
|
+
* 1. The doc provider's own connection-state surface
|
|
313
|
+
* (yjs-provider `DocConnectionState`, via `SessionDocument.
|
|
314
|
+
* onConnectionState` on the SAME `llm` doc the transport writes request
|
|
315
|
+
* markers into): `consecutiveFailures` at/above the provider's own
|
|
316
|
+
* outage bar (`DOC_OUTAGE_LOUD_THRESHOLD` = 3 — the exact state the live
|
|
317
|
+
* log line reports, close code 1002 / an upgrade rejection for a room
|
|
318
|
+
* that no longer exists) means the session's backhaul is unreachable.
|
|
319
|
+
* `consecutiveFailures` resets on every healthy sync, so a recovered
|
|
320
|
+
* blip never trips this.
|
|
321
|
+
*
|
|
322
|
+
* 2. Core Session's own terminal phase machinery (`Session.onPhase` →
|
|
323
|
+
* 'ended' / 'expired' / 'error') — the session lease the pool already
|
|
324
|
+
* tracks (the same signal cli.ts `onSessionEnd` rides).
|
|
325
|
+
*
|
|
326
|
+
* Consumers (cli.ts):
|
|
327
|
+
* - {@link watchSessionGone} per pooled entry; `onGone` → `router.evict`
|
|
328
|
+
* (level-triggered eviction, so the NEXT acquire re-allocates through the
|
|
329
|
+
* existing `sessionFor` path);
|
|
330
|
+
* - {@link guardSessionGone} around each dispatched event stream, so an
|
|
331
|
+
* in-flight (or newly dispatched) request THROWS {@link SessionGoneError}
|
|
332
|
+
* promptly instead of waiting on a doc room that will never answer. The
|
|
333
|
+
* throw surfaces BEFORE any content event, which is exactly what the
|
|
334
|
+
* existing one-shot re-home (rehome.ts) turns into a fresh-session
|
|
335
|
+
* re-dial — no parallel retry mechanism.
|
|
336
|
+
*/
|
|
337
|
+
|
|
338
|
+
/** A latched per-entry session-gone verdict. */
|
|
339
|
+
interface SessionGoneWatch {
|
|
340
|
+
/** The latched verdict — null while the session is (believed) alive. */
|
|
341
|
+
gone(): SessionGoneError | null;
|
|
342
|
+
/**
|
|
343
|
+
* Subscribe to the gone transition. Level-triggered: an already-gone watch
|
|
344
|
+
* invokes the callback immediately. Fires at most once per subscriber.
|
|
345
|
+
* Returns an unsubscribe fn.
|
|
346
|
+
*/
|
|
347
|
+
onGone(cb: (err: SessionGoneError) => void): () => void;
|
|
348
|
+
/** Detach from the session's surfaces (entry teardown / proxy shutdown). */
|
|
349
|
+
dispose(): void;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* THE canonical backhaul factory — the one place a `ProxyClients` seam is
|
|
354
|
+
* built over uRun sessions.
|
|
355
|
+
*
|
|
356
|
+
* This code used to live inside `cli.ts`, which executes `main()` on import
|
|
357
|
+
* and therefore cannot be imported by anything (tests included). Hosting the
|
|
358
|
+
* SAME proxy as a server (`src/hosted/`) needs exactly this wiring, so it was
|
|
359
|
+
* lifted here verbatim rather than forked: `cli.ts` (local, single-tenant) and
|
|
360
|
+
* `hosted/` (shared, multi-tenant) now call the identical
|
|
361
|
+
* {@link buildRouter} / {@link buildClients} pair, and the only difference
|
|
362
|
+
* between them is the {@link BackhaulConfig} they hand it.
|
|
363
|
+
*
|
|
364
|
+
* The two configurations:
|
|
365
|
+
* - LOCAL `defaultApp: <URUN_APP>` — the startup app is the default model
|
|
366
|
+
* and is seeded eagerly, exactly as `urun compat` always behaved.
|
|
367
|
+
* - HOSTED `defaultApp: null` — no default model, nothing seeded;
|
|
368
|
+
* every request must name a deployed model (routing.ts,
|
|
369
|
+
* "NO-DEFAULT MODE") and `auth.apiKey` is the CALLER'S OWN org
|
|
370
|
+
* API key, so every control-plane call and every session dial is
|
|
371
|
+
* org-scoped by the platform itself.
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The `model_catalog` oracle (models.ts) behind routing rule 5 — telling an
|
|
376
|
+
* UNDEPLOYED uRun catalog model apart from a name outside the uRun namespace.
|
|
377
|
+
* `null` disables rule 5; every caller says so out loud when it does.
|
|
378
|
+
*/
|
|
379
|
+
interface CatalogConfig {
|
|
380
|
+
catalogUrl: string;
|
|
381
|
+
anonKey: string;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* The session plus the SDK's OWN terminal release (`Session.end()`), and the
|
|
385
|
+
* core Session surfaces the identity seam rides — all NATIVE @urun-sh/core
|
|
386
|
+
* Session members (`id`, `endsAt`, `onPhase`), typed optional here because
|
|
387
|
+
* the narrow UrunSessionLike does not declare them; the seam fails LOUD when
|
|
388
|
+
* a session object lacks them (no approximation, no minted identity).
|
|
389
|
+
*/
|
|
390
|
+
type OwnedSession = UrunSessionLike & {
|
|
391
|
+
end: () => Promise<unknown>;
|
|
392
|
+
id?: string;
|
|
393
|
+
endsAt?: Date | null;
|
|
394
|
+
onPhase?: (handler: (phase: {
|
|
395
|
+
name: string;
|
|
396
|
+
}) => void) => () => void;
|
|
397
|
+
};
|
|
398
|
+
/**
|
|
399
|
+
* One pooled backhaul: the session, its (stateful) Responses client, and the
|
|
400
|
+
* latched session-gone watch over the session's own transport signals.
|
|
401
|
+
*/
|
|
402
|
+
type PoolEntry = {
|
|
403
|
+
session: OwnedSession;
|
|
404
|
+
responses: UrunResponses;
|
|
405
|
+
gone: SessionGoneWatch;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* PER-CALLER BACKHAULS — the multi-tenant half of the hosted endpoint.
|
|
410
|
+
*
|
|
411
|
+
* One {@link ModelRouter} + {@link ProxyClients} per verified API key, built
|
|
412
|
+
* by the SAME `buildRouter`/`buildClients` factory the local `urun compat`
|
|
413
|
+
* proxy uses (proxy/backhaul.ts). Nothing about routing, pooling, re-homing
|
|
414
|
+
* or session-gone handling is reimplemented here; this module only decides
|
|
415
|
+
* WHICH backhaul a request gets.
|
|
416
|
+
*
|
|
417
|
+
* WHY KEYED ON THE API KEY, NOT THE ORG: two keys in one org must not share a
|
|
418
|
+
* backhaul, because every session dial and every usage event is attributed to
|
|
419
|
+
* the key that minted its token (`usage_events.api_key_id`). Sharing would
|
|
420
|
+
* silently bill one key's traffic to another. Same-org keys therefore get
|
|
421
|
+
* separate pools — a small duplication that keeps attribution honest.
|
|
422
|
+
*
|
|
423
|
+
* CROSS-ORG ISOLATION IS STRUCTURAL, NOT BOOKKEEPING. A tenant's router is
|
|
424
|
+
* built with the caller's OWN key as its only credential, so:
|
|
425
|
+
* - `/v1/models` enumerates `GET {apiUrl}/apps` WITH THAT KEY — the control
|
|
426
|
+
* plane returns that org's apps and no others;
|
|
427
|
+
* - a session dial mints a client token scoped to `<app>/serve` WITH THAT
|
|
428
|
+
* KEY — the control plane refuses to scope it outside the key's org.
|
|
429
|
+
* There is no place in this process where one org's app list or session could
|
|
430
|
+
* be handed to another org's key, because this process never holds a
|
|
431
|
+
* credential that spans orgs.
|
|
432
|
+
*
|
|
433
|
+
* HORIZONTAL SCALE (phase 1). There is deliberately NO shared state plane —
|
|
434
|
+
* no valkey, no shard map, no cross-replica session registry. Two facts make
|
|
435
|
+
* one unnecessary:
|
|
436
|
+
* 1. Nothing here is authoritative. A replica's tenant map is a CACHE of
|
|
437
|
+
* backhauls it happens to have open; a cold replica rebuilds it from the
|
|
438
|
+
* caller's key on the first request. Losing it changes no answer.
|
|
439
|
+
* 2. Replicas converge on the platform's OWN dedupe rather than on a
|
|
440
|
+
* registry of ours: client tokens are minted with a STABLE `subject`
|
|
441
|
+
* derived from the key, and the platform dedupes sessions per
|
|
442
|
+
* (org, actor, app, function) — so replica A and replica B dialing the
|
|
443
|
+
* same (key, app) coalesce onto ONE platform session. That is the
|
|
444
|
+
* library's own mechanism doing the sharding.
|
|
445
|
+
* The yjs/valkey state plane in urun-infra#1490 §7 is therefore NOT built
|
|
446
|
+
* here, and is not silently missing either — it is unnecessary for this
|
|
447
|
+
* phase's correctness and is called out as such in the PR.
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The most distinct callers one replica keeps backhauls open for. On overflow
|
|
452
|
+
* the least-recently-used tenant is closed (`ModelRouter.closeAll()` — the
|
|
453
|
+
* SDK's own terminal release).
|
|
454
|
+
*
|
|
455
|
+
* HONEST TRADEOFF, not a costless one: LRU position is refreshed when a
|
|
456
|
+
* request STARTS, so a tenant part-way through a long generation can become
|
|
457
|
+
* least-recently-used and be evicted under its own in-flight stream. That
|
|
458
|
+
* failure is LOUD, not silent — the existing session-gone machinery turns it
|
|
459
|
+
* into a terminal error event in the lane's native SSE shape, and the next
|
|
460
|
+
* request re-dials. Making it impossible needs in-flight refcounting, i.e.
|
|
461
|
+
* exactly the bespoke bookkeeping this design avoids, so phase 1 sets the cap
|
|
462
|
+
* high enough that reaching it is itself the signal to revisit.
|
|
463
|
+
*
|
|
464
|
+
* There is intentionally no idle TIMER: an idle tenant's pooled sessions are
|
|
465
|
+
* reaped by the PLATFORM'S own idle-close, and the level-triggered
|
|
466
|
+
* session-gone watch (proxy/session-gone.ts) evicts each entry from the pool
|
|
467
|
+
* the moment that happens. So an idle tenant decays to an empty router by
|
|
468
|
+
* itself, through the platform's mechanism, with no reclaim timers of ours.
|
|
469
|
+
*/
|
|
470
|
+
declare const MAX_TENANTS = 512;
|
|
471
|
+
interface TenantRegistryOptions {
|
|
472
|
+
/** The session-gateway base sessions are opened against. */
|
|
473
|
+
baseUrl: string;
|
|
474
|
+
/** The org control-plane API base (`GET {apiUrl}/apps`). */
|
|
475
|
+
apiUrl: string;
|
|
476
|
+
/** The model-catalog oracle for routing rule 5, or null. */
|
|
477
|
+
catalog: CatalogConfig | null;
|
|
478
|
+
/** Injected in tests; production builds real uRun backhauls. */
|
|
479
|
+
build?: (caller: CallerIdentity) => {
|
|
480
|
+
router: ModelRouter<PoolEntry>;
|
|
481
|
+
clients: ProxyClients;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* A stable, non-secret `subject` for one caller's client tokens. Derived from
|
|
486
|
+
* the key so it is identical on every replica (that is the whole point — see
|
|
487
|
+
* the module header's HORIZONTAL SCALE note), and hashed so the raw key never
|
|
488
|
+
* travels inside a token claim or a log line.
|
|
489
|
+
*/
|
|
490
|
+
declare function tenantSubject(apiKey: string): string;
|
|
491
|
+
declare class TenantRegistry {
|
|
492
|
+
private readonly opts;
|
|
493
|
+
/** Insertion-ordered = LRU order, because a hit re-inserts at the end. */
|
|
494
|
+
private readonly tenants;
|
|
495
|
+
constructor(opts: TenantRegistryOptions);
|
|
496
|
+
private build;
|
|
497
|
+
/** The backhaul for THIS caller, opened on first use and reused after. */
|
|
498
|
+
clientsFor(caller: CallerIdentity): ProxyClients;
|
|
499
|
+
/** Live tenant count — the readiness/ops view, and the tests' assertion. */
|
|
500
|
+
get size(): number;
|
|
501
|
+
/** Shutdown: end every pooled session through `Session.end()`. */
|
|
502
|
+
closeAll(): Promise<void>;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The hosted endpoint's configuration.
|
|
507
|
+
*
|
|
508
|
+
* CONFIG MANDATE (org-wide): env vars are NOT a config mechanism. The ONLY
|
|
509
|
+
* things read from the environment here are genuine EXTERNAL INPUTS — which
|
|
510
|
+
* control plane this deployment points at, and the runtime-injected `PORT`.
|
|
511
|
+
* Everything behavioral is a module constant in this file, visible in one
|
|
512
|
+
* place and changed by editing code, never by a knob on a running pod.
|
|
513
|
+
*
|
|
514
|
+
* There is deliberately NO credential input. The service holds no standing
|
|
515
|
+
* platform secret: every upstream call rides the CALLER'S own org API key
|
|
516
|
+
* (see hosted/auth.ts), which is what lets an internet-facing shared endpoint
|
|
517
|
+
* exist without mounting an org-wide credential onto it.
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The container's listen port. The interface contract with the deployment
|
|
522
|
+
* chart is 8080; `PORT` is the runtime-injected override (the one env var
|
|
523
|
+
* class the mandate keeps), never a behavior knob.
|
|
524
|
+
*/
|
|
525
|
+
declare const DEFAULT_PORT = 8080;
|
|
526
|
+
/**
|
|
527
|
+
* The serve function every routed app must expose. HARDCODED, not an env var:
|
|
528
|
+
* "which function name is a serve lane" is a platform contract, not a
|
|
529
|
+
* per-deployment tuning knob, and a pod that could be pointed at a different
|
|
530
|
+
* function would silently serve a different shape of app.
|
|
531
|
+
*/
|
|
532
|
+
declare const SERVE_FUNCTION = "serve";
|
|
533
|
+
/**
|
|
534
|
+
* The address the server binds. A hosted pod must accept traffic from the
|
|
535
|
+
* Service, so this is all interfaces — the opposite of the local CLI proxy's
|
|
536
|
+
* loopback-only bind, and stated explicitly rather than defaulted into.
|
|
537
|
+
*/
|
|
538
|
+
declare const BIND_HOST = "0.0.0.0";
|
|
539
|
+
/** Validated external inputs for one hosted deployment. */
|
|
540
|
+
interface HostedConfig {
|
|
541
|
+
/** Listen port (runtime-injected `PORT`, default {@link DEFAULT_PORT}). */
|
|
542
|
+
port: number;
|
|
543
|
+
/**
|
|
544
|
+
* The SESSION-GATEWAY base (`https://api.urun.sh`) — sessions are opened
|
|
545
|
+
* against it AND the client-token mint (`POST {}/api/client-tokens`), which
|
|
546
|
+
* is also the key-verification call, lives on it.
|
|
547
|
+
*/
|
|
548
|
+
baseUrl: string;
|
|
549
|
+
/** The org control-plane API base (`GET {}/apps`), default `{baseUrl}/v1`. */
|
|
550
|
+
apiUrl: string;
|
|
551
|
+
/** The `model_catalog` oracle for routing rule 5, or null when unconfigured. */
|
|
552
|
+
catalog: CatalogConfig | null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Read and VALIDATE the deployment's external inputs. Every failure is loud
|
|
556
|
+
* at startup: a pod that cannot name its control plane must not come up
|
|
557
|
+
* half-configured and start answering requests it cannot serve.
|
|
558
|
+
*/
|
|
559
|
+
declare function resolveHostedConfig(env?: NodeJS.ProcessEnv): HostedConfig;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* THE HOSTED SHARED ENDPOINT — `inference-proxy`.
|
|
563
|
+
*
|
|
564
|
+
* Owner directive (2026-08-19): "stand up a managed endpoint, authenticated
|
|
565
|
+
* via urun api key, that scales HA, hosts the proxy so that we can just point
|
|
566
|
+
* at it with api key from the org its deployed in and use just like
|
|
567
|
+
* openai/gemini." Plus the binding addition: the Bearer key ALONE determines
|
|
568
|
+
* the org; there is no org id in the URL or in a header.
|
|
569
|
+
*
|
|
570
|
+
* This file is the server SHELL only. It contains no OpenAI/Anthropic
|
|
571
|
+
* protocol code of its own: every `/v1` request is handed to
|
|
572
|
+
* `createProxyRequestHandler` (proxy/server.ts) — the very same handler the
|
|
573
|
+
* local `urun compat proxy` mounts — with a per-request `ProxyClients`
|
|
574
|
+
* resolved from the caller's key. One implementation, two deployments.
|
|
575
|
+
*
|
|
576
|
+
* ROUTES (the contract the deployment chart is built against):
|
|
577
|
+
* GET /healthz liveness — process is up. No upstream calls, ever.
|
|
578
|
+
* GET /readyz readiness — ALSO requires the control plane to be
|
|
579
|
+
* reachable, because a replica that cannot verify API keys
|
|
580
|
+
* or list apps can serve nothing and must be pulled out of
|
|
581
|
+
* the Service rather than answering 502s.
|
|
582
|
+
* * /v1/... the compat surface, org-scoped by the Bearer key.
|
|
583
|
+
* * anything else → 404 in the OpenAI error envelope.
|
|
584
|
+
*
|
|
585
|
+
* WHAT IS DELIBERATELY NOT HERE: `/stats`. The local proxy exposes it for the
|
|
586
|
+
* launcher's reuse probe; on a shared endpoint it would publish one tenant's
|
|
587
|
+
* per-model traffic to every other caller, so the hosted router simply never
|
|
588
|
+
* dispatches it.
|
|
589
|
+
*/
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* How long a readiness verdict is reused. Kubernetes probes every few
|
|
593
|
+
* seconds; without this the replica would hammer the control plane purely to
|
|
594
|
+
* answer its own probes. Short enough that a control-plane outage pulls the
|
|
595
|
+
* pod from the Service within one probe period or two.
|
|
596
|
+
*/
|
|
597
|
+
declare const READINESS_TTL_MS = 5000;
|
|
598
|
+
/** Readiness probes must not hang: an unanswered probe IS a failed probe. */
|
|
599
|
+
declare const READINESS_TIMEOUT_MS = 3000;
|
|
600
|
+
interface HostedProxyOptions extends HostedConfig {
|
|
601
|
+
/** Injected in tests; production uses the real control-plane probe. */
|
|
602
|
+
probeControlPlane?: () => Promise<{
|
|
603
|
+
ready: boolean;
|
|
604
|
+
detail: string;
|
|
605
|
+
}>;
|
|
606
|
+
/** Injected in tests so key verification is exercised without a cluster. */
|
|
607
|
+
resolver?: OrgResolver;
|
|
608
|
+
/** Injected in tests so backhauls are stubbed at the seam. */
|
|
609
|
+
registry?: TenantRegistry;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Build (not listen) the hosted endpoint. The caller owns listen/close, and
|
|
613
|
+
* `closeAll` releases every pooled backhaul through `Session.end()`.
|
|
614
|
+
*/
|
|
615
|
+
declare function createHostedProxy(options: HostedProxyOptions): {
|
|
616
|
+
server: Server;
|
|
617
|
+
closeAll: () => Promise<void>;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export { BIND_HOST, type CallerIdentity, ControlPlaneUnavailableError, DEFAULT_PORT, type HostedConfig, type HostedProxyOptions, MAX_CACHED_KEYS, MAX_TENANTS, ORG_BINDING_TTL_MS, OrgResolver, type OrgResolverOptions, ProxyAuthError, READINESS_TIMEOUT_MS, READINESS_TTL_MS, SERVE_FUNCTION, TenantRegistry, type TenantRegistryOptions, bearerFrom, createHostedProxy, resolveHostedConfig, tenantSubject };
|