@omnicross/core 0.1.2 → 0.1.3
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/auth/GeminiCodeAssistProjectResolver.cjs +6 -1
- package/dist/auth/GeminiCodeAssistProjectResolver.js +6 -1
- package/dist/{chunk-SN3YWBX7.cjs → chunk-2FMI2B6V.cjs} +2329 -533
- package/dist/{chunk-XBSYYZIY.cjs → chunk-3QOSDDNI.cjs} +39 -3
- package/dist/{chunk-2FEVTJWG.js → chunk-4DECIFU7.js} +2162 -366
- package/dist/chunk-4IH4EL7M.cjs +9 -0
- package/dist/chunk-5ENKBSWO.js +311 -0
- package/dist/chunk-6RPZADX3.cjs +92 -0
- package/dist/chunk-AEOZIDEB.cjs +32 -0
- package/dist/chunk-BO4RFWBR.js +89 -0
- package/dist/{chunk-MNYKI4CI.js → chunk-CWM2ZDU5.js} +36 -0
- package/dist/chunk-CZHZGRU7.js +383 -0
- package/dist/{chunk-4NBS6KPV.cjs → chunk-DQ5VAYZ6.cjs} +7 -8
- package/dist/chunk-HUZZZ3JL.js +32 -0
- package/dist/{chunk-QOCNX236.js → chunk-IAXWICZU.js} +24 -1
- package/dist/{chunk-V5KPWNYX.cjs → chunk-JMJBSACD.cjs} +3 -3
- package/dist/chunk-L74VDN4A.cjs +32 -0
- package/dist/{chunk-74TMJA7Z.js → chunk-N5QFKSDO.js} +7 -4
- package/dist/{chunk-LKZJEL6E.cjs → chunk-Q27JY5RG.cjs} +6 -4
- package/dist/{chunk-SPVWWUHX.cjs → chunk-QWBWGODS.cjs} +24 -1
- package/dist/chunk-RPII3E6Z.cjs +145 -0
- package/dist/chunk-S6JGLF3G.cjs +89 -0
- package/dist/chunk-SVXC36MU.cjs +383 -0
- package/dist/chunk-TKJMZBAH.js +32 -0
- package/dist/{chunk-ZSVQT3PW.js → chunk-UBWEBKRC.js} +5 -6
- package/dist/chunk-UNEFIWXI.cjs +32 -0
- package/dist/chunk-VQY5W4YW.cjs +311 -0
- package/dist/chunk-WNY4WMRF.js +32 -0
- package/dist/chunk-XEKD23J5.js +9 -0
- package/dist/chunk-XTEI64OU.js +145 -0
- package/dist/chunk-XX6NQJMA.js +92 -0
- package/dist/{chunk-N3V2J5ZO.cjs → chunk-Y6OM7EV7.cjs} +7 -4
- package/dist/{chunk-745DV5FL.js → chunk-Y7FO65VT.js} +3 -3
- package/dist/{chunk-PXUJF5HS.js → chunk-ZY6KU6P3.js} +4 -2
- package/dist/completion/CompletionService.cjs +22 -15
- package/dist/completion/CompletionService.js +21 -14
- package/dist/completion/NativeSearchInjector.cjs +3 -3
- package/dist/completion/NativeSearchInjector.js +2 -2
- package/dist/completion/openrouter-headers.cjs +3 -6
- package/dist/completion/openrouter-headers.d.cts +9 -12
- package/dist/completion/openrouter-headers.d.ts +9 -12
- package/dist/completion/openrouter-headers.js +2 -5
- package/dist/completion/openrouter-models.cjs +4 -3
- package/dist/completion/openrouter-models.js +2 -1
- package/dist/completion.cjs +22 -15
- package/dist/completion.js +21 -14
- package/dist/index.cjs +139 -17
- package/dist/index.d.cts +40 -10
- package/dist/index.d.ts +40 -10
- package/dist/index.js +146 -24
- package/dist/outbound-api/routeResolver.cjs +5 -2
- package/dist/outbound-api/routeResolver.d.cts +8 -2
- package/dist/outbound-api/routeResolver.d.ts +8 -2
- package/dist/outbound-api/routeResolver.js +4 -1
- package/dist/outbound-api/types.cjs +6 -1
- package/dist/outbound-api/types.d.cts +11 -144
- package/dist/outbound-api/types.d.ts +11 -144
- package/dist/outbound-api/types.js +6 -0
- package/dist/outbound-api.cjs +133 -15
- package/dist/outbound-api.d.cts +786 -25
- package/dist/outbound-api.d.ts +786 -25
- package/dist/outbound-api.js +137 -19
- package/dist/pipeline/AuthSource.d.cts +23 -1
- package/dist/pipeline/AuthSource.d.ts +23 -1
- package/dist/pipeline/LlmConfigProviderAuth.cjs +3 -3
- package/dist/pipeline/LlmConfigProviderAuth.js +2 -2
- package/dist/pipeline/SubscriptionAccountHealth.cjs +22 -0
- package/dist/pipeline/SubscriptionAccountHealth.d.cts +235 -0
- package/dist/pipeline/SubscriptionAccountHealth.d.ts +235 -0
- package/dist/pipeline/SubscriptionAccountHealth.js +22 -0
- package/dist/pipeline/SubscriptionAuthSource.cjs +2 -2
- package/dist/pipeline/SubscriptionAuthSource.d.cts +3 -2
- package/dist/pipeline/SubscriptionAuthSource.d.ts +3 -2
- package/dist/pipeline/SubscriptionAuthSource.js +1 -1
- package/dist/pipeline/SubscriptionAuthStrategy.d.cts +24 -1
- package/dist/pipeline/SubscriptionAuthStrategy.d.ts +24 -1
- package/dist/pipeline/auditSink.cjs +14 -0
- package/dist/pipeline/auditSink.d.cts +48 -0
- package/dist/pipeline/auditSink.d.ts +48 -0
- package/dist/pipeline/auditSink.js +14 -0
- package/dist/pipeline/billingEmit.cjs +14 -0
- package/dist/pipeline/billingEmit.d.cts +49 -0
- package/dist/pipeline/billingEmit.d.ts +49 -0
- package/dist/pipeline/billingEmit.js +14 -0
- package/dist/ports/usage-event-store.d.cts +26 -1
- package/dist/ports/usage-event-store.d.ts +26 -1
- package/dist/ports.d.cts +7 -1
- package/dist/ports.d.ts +7 -1
- package/dist/provider-proxy/ProviderProxy.cjs +22 -15
- package/dist/provider-proxy/ProviderProxy.js +21 -14
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.cjs +15 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.cts +121 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.d.ts +121 -0
- package/dist/provider-proxy/identity/SubscriptionIdentityStore.js +15 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.cjs +20 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.d.cts +89 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.d.ts +89 -0
- package/dist/provider-proxy/identity/fingerprintHeaders.js +20 -0
- package/dist/provider-proxy/ingress/providerProxyShared.cjs +22 -15
- package/dist/provider-proxy/ingress/providerProxyShared.d.cts +11 -2
- package/dist/provider-proxy/ingress/providerProxyShared.d.ts +11 -2
- package/dist/provider-proxy/ingress/providerProxyShared.js +21 -14
- package/dist/provider-proxy/matchText.cjs +4 -2
- package/dist/provider-proxy/matchText.d.cts +14 -1
- package/dist/provider-proxy/matchText.d.ts +14 -1
- package/dist/provider-proxy/matchText.js +3 -1
- package/dist/provider-proxy/types.d.cts +23 -0
- package/dist/provider-proxy/types.d.ts +23 -0
- package/dist/provider-proxy.cjs +22 -15
- package/dist/provider-proxy.js +21 -14
- package/dist/{routeResolver-HE-ZO0fO.d.ts → routeResolver-B1NglTfe.d.ts} +36 -10
- package/dist/{routeResolver-BrbK6ja9.d.cts → routeResolver-CbG5uBgm.d.cts} +36 -10
- package/dist/transformer/transformers.cjs +7 -7
- package/dist/transformer/transformers.js +7 -7
- package/dist/transformer.cjs +5 -5
- package/dist/transformer.js +4 -4
- package/dist/types-6OUY_xfA.d.ts +830 -0
- package/dist/types-aBFFXoBz.d.cts +830 -0
- package/dist/usage/usage-recorder.cjs +2 -2
- package/dist/usage/usage-recorder.d.cts +19 -1
- package/dist/usage/usage-recorder.d.ts +19 -1
- package/dist/usage/usage-recorder.js +1 -1
- package/dist/usage.cjs +2 -2
- package/dist/usage.js +1 -1
- package/package.json +4 -2
- package/dist/chunk-3MEACFK3.js +0 -193
- package/dist/chunk-6VIXXLMX.cjs +0 -14
- package/dist/chunk-E3WHL7CO.js +0 -14
- package/dist/chunk-G2FUJNA2.cjs +0 -193
- package/dist/{chunk-UYPEN5XE.cjs → chunk-FFALUUZZ.cjs} +5 -5
- package/dist/{chunk-O466Y272.js → chunk-MQNDWYPN.js} +6 -6
package/dist/outbound-api.d.ts
CHANGED
|
@@ -1,24 +1,94 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { VoucherConfig } from '@omnicross/contracts/voucher-types';
|
|
2
|
+
import { i as KindMappedEndpoint, j as ModelKind, q as OutboundEndpoint, b as EndpointRoutingConfig, o as OutboundApiServerConfig, U as UserMessageQueueConfig, a as ConcurrencyQueueConfig, l as OutboundApiDeps, p as OutboundApiServerStatus, r as OutboundFormatUrls, A as AccountProbeConfig, F as FingerprintConfig, k as ModelPrefixTargets, v as OutboundProxyConfig, P as ModelRef, K as KeyCostLimits, S as ModelRestriction, O as OutboundKeyDb, m as OutboundApiKeyCreated, R as RequestRole } from './types-6OUY_xfA.js';
|
|
3
|
+
export { C as ChatDispatchMode, E as ENDPOINT_MODEL_KINDS, c as KeyExpiryInput, d as KeyExpiryResult, e as KeySpend, f as KeySpendReader, g as KeySpendSeeder, h as KeySpendTracker, M as MessagesModelKind, n as OutboundApiKeyInfo, s as OutboundKeyActivationMode, t as OutboundKeyDbRow, u as OutboundKeyPolicy, Q as QuotaDecision, w as ResponsesModelKind, V as VoucherCreateInput, x as VoucherDb, y as checkKeyQuota, z as computeKeyExpiry, B as computeVoucherGrant, D as generateVoucherCode, G as hashVoucherCode, H as newVoucherId, I as startOfLocalDay, J as startOfLocalWeek, L as toVoucherInfo, N as voucherCodePrefix } from './types-6OUY_xfA.js';
|
|
4
|
+
import { ProxyConfig } from '@omnicross/contracts/account-tokens-types';
|
|
5
|
+
import { AuditConfig } from '@omnicross/contracts/audit-types';
|
|
6
|
+
import { BillingConfig } from '@omnicross/contracts/billing-types';
|
|
7
|
+
import { WebhookDestination, WebhookConfig } from '@omnicross/contracts/webhook-types';
|
|
8
|
+
import http from 'node:http';
|
|
3
9
|
import { IngressFormat } from './provider-proxy/types.js';
|
|
4
|
-
export { S as SUBSCRIPTION_PROVIDER_IDS, e as endpointSupportsSubscription, i as isSubscriptionProviderId, r as resolveRoute } from './routeResolver-
|
|
10
|
+
export { S as SUBSCRIPTION_PROVIDER_IDS, e as endpointSupportsSubscription, i as isSubscriptionProviderId, p as parseModelRef, a as pickModelRefFromList, r as resolveRoute } from './routeResolver-B1NglTfe.js';
|
|
11
|
+
import '@omnicross/contracts/health-logging-types';
|
|
12
|
+
import './logger-4GvQNzhE.js';
|
|
5
13
|
import './ports/provider-config-source.js';
|
|
6
14
|
import '@omnicross/contracts/llm-config';
|
|
7
15
|
import './transformer/types.js';
|
|
8
16
|
import './transformer/TransformerService.js';
|
|
9
17
|
import './ProviderProxy-C-xqrkKi.js';
|
|
10
|
-
import 'node:http';
|
|
11
18
|
import '@omnicross/contracts/completion-types';
|
|
12
19
|
import '@omnicross/contracts/subscription-types';
|
|
13
20
|
import '@omnicross/contracts/usage-types';
|
|
14
21
|
import './completion/ApiKeyPoolService.js';
|
|
15
|
-
import './logger-4GvQNzhE.js';
|
|
16
22
|
import './pipeline/AuthSource.js';
|
|
17
23
|
import './pipeline/SubscriptionAuthSource.js';
|
|
18
24
|
import './pipeline/SubscriptionAuthStrategy.js';
|
|
19
25
|
import './ports/web-search-backend.js';
|
|
20
26
|
import '@omnicross/contracts/websearch-types';
|
|
21
27
|
|
|
28
|
+
/**
|
|
29
|
+
* kindDetection — classify an outbound request's model KIND for the kind-mapped
|
|
30
|
+
* endpoints (`messages`/`responses`) and validate their config completeness
|
|
31
|
+
* (`outbound-api-server`, model-kind-mapping contract).
|
|
32
|
+
*
|
|
33
|
+
* The kind-mapped endpoints route by a version-INDEPENDENT model KIND rather
|
|
34
|
+
* than by role: the user configures one upstream ref per kind
|
|
35
|
+
* ({@link ENDPOINT_MODEL_KINDS}) and an incoming versioned client id
|
|
36
|
+
* (`claude-opus-4-8-2026xxxx`) is classified to its kind (`opus`) so CLI
|
|
37
|
+
* upgrades need no reconfig. `chat`/`gemini` stay on `roleDetection`.
|
|
38
|
+
*
|
|
39
|
+
* Pure module — no I/O. The router decides, per endpoint, whether to call
|
|
40
|
+
* `detectModelKind` (messages/responses) or `detectRequestRole` (chat/gemini);
|
|
41
|
+
* the routing RESOLUTION and the startup-gate ENFORCEMENT live downstream.
|
|
42
|
+
*
|
|
43
|
+
* @module outbound-api/kindDetection
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Narrow an endpoint to the kind-mapped set (`messages`/`responses`). `chat`
|
|
48
|
+
* and `gemini` are role-based and return false.
|
|
49
|
+
*/
|
|
50
|
+
declare function isKindMappedEndpoint(endpoint: OutboundEndpoint): endpoint is KindMappedEndpoint;
|
|
51
|
+
/** The canonical kinds for a kind-mapped endpoint (in declaration order). */
|
|
52
|
+
declare function modelKindsForEndpoint(endpoint: KindMappedEndpoint): readonly ModelKind[];
|
|
53
|
+
/**
|
|
54
|
+
* Extract the version-INDEPENDENT model KIND for a kind-mapped endpoint.
|
|
55
|
+
* - `messages`: the FIRST id token that is one of {fable,opus,sonnet,haiku};
|
|
56
|
+
* `undefined` when no token matches (the unmatched-kind fallback is SERVING's
|
|
57
|
+
* decision, not core's).
|
|
58
|
+
* - `responses`: `mini` when {@link isBackgroundTierModel}; else `codex`
|
|
59
|
+
* (codex is the else-branch — a present id always resolves).
|
|
60
|
+
* - Any empty/blank id ⇒ `undefined`.
|
|
61
|
+
*
|
|
62
|
+
* Reuses `normalizeModelId` + the shared `/[-._:/\s]+/` tokenizer
|
|
63
|
+
* (`modelTokens`) + the small-tier token set (`isBackgroundTierModel`) so the
|
|
64
|
+
* kind and role detectors stay token-boundary-consistent.
|
|
65
|
+
*/
|
|
66
|
+
declare function detectModelKind(endpoint: KindMappedEndpoint, requestedModelId: string | undefined): ModelKind | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The kinds THIS endpoint declares but leaves unconfigured (blank/absent ref).
|
|
69
|
+
* Role-based endpoints (`chat`/`gemini`) have no declared kinds → `[]`.
|
|
70
|
+
* Absent and blank refs are treated identically; a malformed-but-non-blank ref
|
|
71
|
+
* is a serving-time concern, not a startup-gate one.
|
|
72
|
+
*/
|
|
73
|
+
declare function validateEndpointModelConfig(config: EndpointRoutingConfig): ModelKind[];
|
|
74
|
+
/** One incomplete kind-mapped endpoint and the kinds it is missing. */
|
|
75
|
+
interface EndpointModelConfigError {
|
|
76
|
+
endpoint: KindMappedEndpoint;
|
|
77
|
+
missingKinds: ModelKind[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Server-level completeness, PER-ENDPOINT: a kind-mapped endpoint whose
|
|
81
|
+
* declared kinds are ALL blank/absent counts as UNCONFIGURED — the operator
|
|
82
|
+
* simply doesn't use that endpoint, so it does NOT block startup (its requests
|
|
83
|
+
* 503 per-request instead). Only a PARTIALLY configured endpoint (some kinds
|
|
84
|
+
* set, some blank — a real config mistake that would silently misroute) is
|
|
85
|
+
* returned as an error. Empty result ⇒ the config satisfies the startup gate.
|
|
86
|
+
*
|
|
87
|
+
* Serving consumes this for the gate ENFORCEMENT; it MAY tighten to an
|
|
88
|
+
* all-endpoints-strict policy by composing {@link validateEndpointModelConfig}.
|
|
89
|
+
*/
|
|
90
|
+
declare function validateServerModelConfig(config: OutboundApiServerConfig): EndpointModelConfigError[];
|
|
91
|
+
|
|
22
92
|
/**
|
|
23
93
|
* OutboundApiServer — the external-facing HTTP listener for the outbound API
|
|
24
94
|
* server (`outbound-api-server`, design D1/D4/D5).
|
|
@@ -51,6 +121,24 @@ interface ApplyConfigInput {
|
|
|
51
121
|
networkBinding: boolean;
|
|
52
122
|
endpoints: EndpointRoutingConfig[];
|
|
53
123
|
port?: number;
|
|
124
|
+
/** User-message serial-queue segment (normalized/defaulted by core). */
|
|
125
|
+
userMessageQueue?: UserMessageQueueConfig;
|
|
126
|
+
/** Per-key concurrency-queue segment (normalized/defaulted by core). */
|
|
127
|
+
concurrencyQueue?: ConcurrencyQueueConfig;
|
|
128
|
+
/** Voucher segment (voucher-redemption #9). Read live per request; absent ⇒
|
|
129
|
+
* disabled ⇒ the `/redeem` endpoint is inert (zero regression). */
|
|
130
|
+
voucher?: VoucherConfig;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Thrown by {@link OutboundApiServer.applyConfig} when an ENABLED server is asked
|
|
134
|
+
* to bind with an INCOMPLETE model-kind map (the "未配置 → 无法启动接口服务" gate,
|
|
135
|
+
* design D6). Carries the per-endpoint missing kinds so the daemon/UI can render
|
|
136
|
+
* an actionable message; the server does NOT bind. Exported from the barrel so
|
|
137
|
+
* the daemon (surface) can `instanceof`-narrow it.
|
|
138
|
+
*/
|
|
139
|
+
declare class OutboundApiConfigError extends Error {
|
|
140
|
+
readonly missing: EndpointModelConfigError[];
|
|
141
|
+
constructor(missing: EndpointModelConfigError[]);
|
|
54
142
|
}
|
|
55
143
|
declare class OutboundApiServer {
|
|
56
144
|
private readonly deps;
|
|
@@ -60,7 +148,25 @@ declare class OutboundApiServer {
|
|
|
60
148
|
private boundPort;
|
|
61
149
|
private boundAddr;
|
|
62
150
|
private endpoints;
|
|
151
|
+
private userMessageQueue;
|
|
152
|
+
private concurrencyQueue;
|
|
153
|
+
private voucherConfig;
|
|
63
154
|
private readonly rateLimiter;
|
|
155
|
+
/**
|
|
156
|
+
* Redeem-attempt limiter (voucher-redemption #9, design D6) — a SEPARATE bucket
|
|
157
|
+
* from the traffic `rateLimiter`, keyed by the authenticating key id, so
|
|
158
|
+
* brute-forcing `CC_` codes is throttled (a handful/min) without touching the
|
|
159
|
+
* per-key request rate. Conservative fixed defaults (10 / 60s).
|
|
160
|
+
*/
|
|
161
|
+
private readonly redeemLimiter;
|
|
162
|
+
/**
|
|
163
|
+
* Per-key redeem mutex (voucher-redemption #9, MJ1 fix). One instance for the
|
|
164
|
+
* server's lifetime so concurrent redeem REQUESTS for the same key serialize
|
|
165
|
+
* (relative grant increments accumulate instead of clobbering a snapshot).
|
|
166
|
+
*/
|
|
167
|
+
private readonly redeemMutex;
|
|
168
|
+
private readonly serialQueue;
|
|
169
|
+
private readonly concurrencyGate;
|
|
64
170
|
constructor(deps: OutboundApiDeps,
|
|
65
171
|
/** Called when the actual bound port differs from the requested one. */
|
|
66
172
|
onPortChange?: ((port: number) => void) | undefined);
|
|
@@ -76,10 +182,37 @@ declare class OutboundApiServer {
|
|
|
76
182
|
private listen;
|
|
77
183
|
/** Per-request handler. Auth is enforced on EVERY request (incl. loopback). */
|
|
78
184
|
private onRequest;
|
|
185
|
+
/**
|
|
186
|
+
* Serve `GET|HEAD /health` (+ `/healthz`) from the injected provider, returning
|
|
187
|
+
* true when it handled the request. 200 when `ok`, else 503; secret-free body.
|
|
188
|
+
*/
|
|
189
|
+
private tryServeHealth;
|
|
190
|
+
/** Route an info lifecycle line through the injected logger, else `console.log`
|
|
191
|
+
* (byte-identical legacy fallback when no logger is wired). */
|
|
192
|
+
private logInfo;
|
|
193
|
+
/** Route an error lifecycle line through the injected logger, else `console.error`. */
|
|
194
|
+
private logError;
|
|
79
195
|
/** Stop the listener and release the port. */
|
|
80
196
|
stop(): Promise<void>;
|
|
81
197
|
/** A live status snapshot for the Settings tab. */
|
|
82
198
|
getStatus(): OutboundApiServerStatus;
|
|
199
|
+
/**
|
|
200
|
+
* Live queue-occupancy snapshot (only active entries). This getter's name +
|
|
201
|
+
* shape are FROZEN — `omnicross-uqc-daemon` spreads it into its `/status`
|
|
202
|
+
* response; the existing {@link getStatus} shape is deliberately NOT changed.
|
|
203
|
+
*/
|
|
204
|
+
getQueueStatus(): {
|
|
205
|
+
serial: Array<{
|
|
206
|
+
providerId: string;
|
|
207
|
+
holding: boolean;
|
|
208
|
+
waiting: number;
|
|
209
|
+
}>;
|
|
210
|
+
concurrency: Array<{
|
|
211
|
+
apiKeyId: string;
|
|
212
|
+
active: number;
|
|
213
|
+
waiting: number;
|
|
214
|
+
}>;
|
|
215
|
+
};
|
|
83
216
|
}
|
|
84
217
|
/** Build the four format endpoint URLs for a base URL. */
|
|
85
218
|
declare function formatUrls(base: string): OutboundFormatUrls;
|
|
@@ -90,20 +223,137 @@ declare function formatUrls(base: string): OutboundFormatUrls;
|
|
|
90
223
|
*
|
|
91
224
|
* The config (`{ enabled, networkBinding, endpoints, port }`) is persisted via
|
|
92
225
|
* a small key/value store (the app SettingsService) under a single key, so it
|
|
93
|
-
* survives restart. Defaults: disabled, loopback, four endpoints
|
|
94
|
-
*
|
|
95
|
-
*
|
|
226
|
+
* survives restart. Defaults: disabled, loopback, four blank endpoints +
|
|
227
|
+
* `useSubscription` OFF, default port. The per-endpoint shape is heterogeneous:
|
|
228
|
+
* kind-mapped endpoints (`messages`/`responses`) carry a blank `modelMap` (one
|
|
229
|
+
* key per declared kind); role-based endpoints (`chat`/`gemini`) carry blank
|
|
230
|
+
* `defaultModel`/`backgroundModel`. NO legacy migration — `normalizeServerConfig`
|
|
231
|
+
* drops unknown/legacy fields (incl. `visionModel`) and fills blanks. Shared by
|
|
232
|
+
* the router and the bootstrap wiring so both read/write the same shape.
|
|
96
233
|
*
|
|
97
234
|
* @module outbound-api/apiServerConfig
|
|
98
235
|
*/
|
|
99
236
|
|
|
100
237
|
/** The settings key the config persists under. */
|
|
101
238
|
declare const OUTBOUND_API_SERVER_CONFIG_KEY = "outboundApiServer.config";
|
|
239
|
+
/**
|
|
240
|
+
* Frozen defaults for the user-message serial queue segment (SSOT). Note the
|
|
241
|
+
* `waitTimeoutMs` default is **60000** — the office-hours draft's 30000 is
|
|
242
|
+
* superseded by the user's拍板 / planning-context §COMMITTED.
|
|
243
|
+
*/
|
|
244
|
+
declare const DEFAULT_USER_MESSAGE_QUEUE: UserMessageQueueConfig;
|
|
245
|
+
/** Frozen defaults for the per-key concurrency queue segment (SSOT). */
|
|
246
|
+
declare const DEFAULT_CONCURRENCY_QUEUE: ConcurrencyQueueConfig;
|
|
247
|
+
/**
|
|
248
|
+
* Frozen defaults for the scheduled account-probe segment (SSOT,
|
|
249
|
+
* subscription-account-probe #8). Default OFF (zero regression); a 15-min cadence,
|
|
250
|
+
* multi-account-only, short timeout, small rolling history, staggered — every knob
|
|
251
|
+
* a load-safety valve (see `AccountProbeConfig`).
|
|
252
|
+
*/
|
|
253
|
+
declare const DEFAULT_ACCOUNT_PROBE: AccountProbeConfig;
|
|
254
|
+
/** Fill + range-CLAMP the account-probe segment to the frozen defaults. */
|
|
255
|
+
declare function normalizeAccountProbe(raw: Partial<OutboundApiServerConfig> | undefined | null): AccountProbeConfig;
|
|
256
|
+
/**
|
|
257
|
+
* Fill + range-CLAMP the request-audit segment to the frozen defaults
|
|
258
|
+
* (request-audit-log, design D2). Lenient like the other segment normalizers:
|
|
259
|
+
* `enabled`/`captureBodies`/`trustForwardedFor` coerce to booleans (default
|
|
260
|
+
* false), `maxBodyBytes` clamps to `[256, 1_048_576]`, `retentionDays` clamps to
|
|
261
|
+
* `[1, 365]`. Default (all-off) ⇒ no capture ⇒ zero regression.
|
|
262
|
+
*/
|
|
263
|
+
declare function normalizeAudit(raw: Partial<OutboundApiServerConfig> | undefined | null): AuditConfig;
|
|
264
|
+
/**
|
|
265
|
+
* Fill + range-CLAMP the billing segment to the frozen defaults
|
|
266
|
+
* (billing-event-stream, design D6). Lenient like `normalizeAudit`: `enabled`
|
|
267
|
+
* coerces to a boolean (default false); `endpoint`/`secret` are carried only when
|
|
268
|
+
* non-empty strings (a blank/absent `endpoint` ⇒ ledger-only mode); `maxRetryAgeMs`
|
|
269
|
+
* clamps to `[60_000, 30 days]`. Default (off) ⇒ no publish ⇒ zero regression.
|
|
270
|
+
*/
|
|
271
|
+
declare function normalizeBilling(raw: Partial<OutboundApiServerConfig> | undefined | null): BillingConfig;
|
|
272
|
+
/**
|
|
273
|
+
* Frozen defaults for the client-fingerprint segment (SSOT,
|
|
274
|
+
* subscription-client-fingerprint #7). Default OFF ⇒ no capture/replay ⇒
|
|
275
|
+
* byte-identical outbound headers.
|
|
276
|
+
*/
|
|
277
|
+
declare const DEFAULT_FINGERPRINT: FingerprintConfig;
|
|
278
|
+
/**
|
|
279
|
+
* Fill the client-fingerprint segment to the frozen defaults. Lenient like the
|
|
280
|
+
* other segment normalizers: `enabled` coerces to a boolean (default false); `ua`
|
|
281
|
+
* is carried only when a non-empty trimmed string (a blank/absent baseline stays
|
|
282
|
+
* absent). Default (off) ⇒ zero regression. Carries NO secret.
|
|
283
|
+
*/
|
|
284
|
+
declare function normalizeFingerprint(raw: Partial<OutboundApiServerConfig> | undefined | null): FingerprintConfig;
|
|
285
|
+
/**
|
|
286
|
+
* Fill the voucher segment to the frozen defaults (voucher-redemption #9). Lenient
|
|
287
|
+
* like the other segment normalizers: `enabled` coerces to a boolean (default
|
|
288
|
+
* false). Default (off) ⇒ the redeem endpoint is inert ⇒ zero regression. Carries
|
|
289
|
+
* NO secret (codes are hashed at rest in the separate voucher store).
|
|
290
|
+
*/
|
|
291
|
+
declare function normalizeVoucher(raw: Partial<OutboundApiServerConfig> | undefined | null): VoucherConfig;
|
|
292
|
+
/**
|
|
293
|
+
* Validate ONE `ProxyConfig` (upstream-proxy). Returns the cleaned descriptor or
|
|
294
|
+
* `undefined` (drop) when malformed. Lenient like the other segment normalizers:
|
|
295
|
+
* - `{ url }` — a non-empty string URL (trimmed).
|
|
296
|
+
* - structured — `type` ∈ {http,https,socks5} + non-empty `host` + a
|
|
297
|
+
* finite integer `port` in `1..65535`. `username`/`password`
|
|
298
|
+
* are non-empty-string-or-omit (may be `enc:`/`$ENV` at load
|
|
299
|
+
* — the secret box decrypts afterwards).
|
|
300
|
+
* Never throws.
|
|
301
|
+
*/
|
|
302
|
+
declare function normalizeProxyConfig(raw: unknown): ProxyConfig | undefined;
|
|
303
|
+
/**
|
|
304
|
+
* Validate the optional `proxy` segment (upstream-proxy). Drops malformed entries
|
|
305
|
+
* (a bad `global` or a bad `byProvider[*]` value/key is dropped, never thrown).
|
|
306
|
+
* Returns `undefined` when nothing valid remains — a missing/empty proxy segment
|
|
307
|
+
* stays ABSENT (zero-config = direct fetch; unlike `accountHealth`, no default is
|
|
308
|
+
* synthesized).
|
|
309
|
+
*/
|
|
310
|
+
declare function normalizeProxySegment(raw: unknown): OutboundProxyConfig | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* Validate ONE webhook destination (webhook-notifications). Returns the cleaned
|
|
313
|
+
* descriptor or `undefined` (drop) when malformed — lenient like the proxy
|
|
314
|
+
* normalizer, never throws:
|
|
315
|
+
* - `id` — a non-empty trimmed string.
|
|
316
|
+
* - `type` — ∈ {custom, feishu}.
|
|
317
|
+
* - `url` — a non-empty trimmed string.
|
|
318
|
+
* - `secret` — non-empty-string-or-omit (may be `enc:`/`$ENV` at load — the
|
|
319
|
+
* settings-store secret box decrypts afterwards).
|
|
320
|
+
* - `events` — kept only when a non-empty array of known kinds (unknown kinds
|
|
321
|
+
* dropped); an absent/empty filter means "all kinds".
|
|
322
|
+
* - `enabled` — coerced boolean (default true — a destination in the list is on
|
|
323
|
+
* unless explicitly disabled).
|
|
324
|
+
*/
|
|
325
|
+
declare function normalizeWebhookDestination(raw: unknown): WebhookDestination | undefined;
|
|
326
|
+
/**
|
|
327
|
+
* Validate the optional `webhook` segment (webhook-notifications). Drops
|
|
328
|
+
* malformed destinations; `enabled` defaults false. Returns `undefined` when the
|
|
329
|
+
* segment is absent/non-object — a missing webhook segment stays ABSENT (no sink
|
|
330
|
+
* wired ⇒ zero regression), unlike `accountHealth` no default is synthesized. A
|
|
331
|
+
* present segment with `enabled` present OR any valid destination is kept.
|
|
332
|
+
*/
|
|
333
|
+
declare function normalizeWebhookSegment(raw: unknown): WebhookConfig | undefined;
|
|
334
|
+
/**
|
|
335
|
+
* Fill + range-CLAMP the two queue segments to the frozen defaults. Lenient:
|
|
336
|
+
* out-of-range persisted numerics are clamped to the nearest bound, never
|
|
337
|
+
* thrown — strict validation is the daemon admin PUT's job. `enabled` coerces
|
|
338
|
+
* to a boolean (default false).
|
|
339
|
+
*/
|
|
340
|
+
declare function normalizeQueueSegments(raw: Partial<OutboundApiServerConfig> | undefined | null): {
|
|
341
|
+
userMessageQueue: UserMessageQueueConfig;
|
|
342
|
+
concurrencyQueue: ConcurrencyQueueConfig;
|
|
343
|
+
};
|
|
102
344
|
/** Structural subset of the settings store the config loader needs. */
|
|
103
345
|
interface ApiServerSettingsStore {
|
|
104
346
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
105
347
|
set<T = unknown>(key: string, value: T): Promise<void>;
|
|
106
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Validate the optional `chat` prefix-target map (openai-chat-bridge #11). Keeps
|
|
351
|
+
* ONLY the three known prefixes (`claude`/`gpt`/`gemini`) whose value is a
|
|
352
|
+
* non-empty trimmed `"providerId,modelId"` string; drops everything else. Returns
|
|
353
|
+
* `undefined` when nothing valid remains (a prefix-mode chat endpoint with no
|
|
354
|
+
* targets simply routes nothing until configured). Never throws.
|
|
355
|
+
*/
|
|
356
|
+
declare function normalizePrefixTargets(raw: unknown): ModelPrefixTargets | undefined;
|
|
107
357
|
/** The default server config: disabled, loopback, four blank endpoints. */
|
|
108
358
|
declare function defaultServerConfig(): OutboundApiServerConfig;
|
|
109
359
|
/**
|
|
@@ -119,6 +369,391 @@ declare function saveServerConfig(store: ApiServerSettingsStore, config: Outboun
|
|
|
119
369
|
/** Apply a partial patch to a config, returning the merged whole. */
|
|
120
370
|
declare function mergeServerConfig(current: OutboundApiServerConfig, patch: Partial<OutboundApiServerConfig>): OutboundApiServerConfig;
|
|
121
371
|
|
|
372
|
+
/**
|
|
373
|
+
* auditRedact — the pure secret-redaction pass for captured audit bodies
|
|
374
|
+
* (request-audit-log, design D3/D7).
|
|
375
|
+
*
|
|
376
|
+
* Body capture is opt-in and inherently sensitive: a prompt or tool argument can
|
|
377
|
+
* embed a secret. {@link redactAuditText} is run over EVERY captured body (and
|
|
378
|
+
* any other free text stored in a record) BEFORE it is persisted — defense in
|
|
379
|
+
* depth on top of "request headers are NEVER captured" (so an Authorization /
|
|
380
|
+
* x-api-key HEADER value never enters a record in the first place).
|
|
381
|
+
*
|
|
382
|
+
* It masks the obvious, high-confidence secret shapes ONLY (frozen list) — never
|
|
383
|
+
* a broad heuristic that would gut a legitimate prompt:
|
|
384
|
+
* - `sk-…` API keys (covers `sk-ant-…`, `sk-omnicross-…`, `sk-proj-…`, OpenAI).
|
|
385
|
+
* - `Bearer <token>` authorization values.
|
|
386
|
+
* - `x-api-key` / `api-key` / `authorization` values written inline as text.
|
|
387
|
+
* - Google `AIza…` API keys.
|
|
388
|
+
*
|
|
389
|
+
* Pure + allocation-light + never throws.
|
|
390
|
+
*
|
|
391
|
+
* @module @omnicross/core/outbound-api/auditRedact
|
|
392
|
+
*/
|
|
393
|
+
/** The sentinel a masked secret is replaced with. */
|
|
394
|
+
declare const AUDIT_REDACTED = "***REDACTED***";
|
|
395
|
+
/**
|
|
396
|
+
* Mask obvious secrets in `text`. Pure; returns the input unchanged when it holds
|
|
397
|
+
* no recognized secret shape. Never throws (a bad input returns `''`).
|
|
398
|
+
*/
|
|
399
|
+
declare function redactAuditText(text: string): string;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* auditCapture — the outbound server's post-response audit capture hook
|
|
403
|
+
* (request-audit-log, design D1/D3/D5).
|
|
404
|
+
*
|
|
405
|
+
* {@link beginAuditCapture} is called at the TOP of `handleOutboundRequest`. It
|
|
406
|
+
* reads the active capture config from the core slot; when audit is DISABLED it
|
|
407
|
+
* returns `null` immediately (one slot read — no assembly, no listener, zero
|
|
408
|
+
* regression). When enabled it:
|
|
409
|
+
* - registers a one-shot `res.on('close')` listener that, at response end,
|
|
410
|
+
* assembles the {@link AuditRecord} from the http `req` (IP via the socket, or
|
|
411
|
+
* a trusted `X-Forwarded-For` only when configured — LEAD OQ1 anti-spoof; UA;
|
|
412
|
+
* method; path with the query string DROPPED), the response `statusCode`, the
|
|
413
|
+
* elapsed ms, and the fields the handler fills in (keyId / model / provider /
|
|
414
|
+
* error / bodies) — then hands it to the fire-and-forget `recordAudit`;
|
|
415
|
+
* - when `captureBodies` is ALSO on, wraps `res.write`/`res.end` to accumulate a
|
|
416
|
+
* BOUNDED response body (metadata-only for a streaming `text/event-stream`
|
|
417
|
+
* response — a full stream is unbounded), and truncates+redacts both bodies.
|
|
418
|
+
*
|
|
419
|
+
* Request HEADERS are NEVER read into a record (Authorization / x-api-key live
|
|
420
|
+
* there). Every stored body passes through {@link redactAuditText}. The assembly
|
|
421
|
+
* is wrapped so a capture failure can never surface into the relay path.
|
|
422
|
+
*
|
|
423
|
+
* @module @omnicross/core/outbound-api/auditCapture
|
|
424
|
+
*/
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The mutable context the request handler enriches as it progresses. `null` is
|
|
428
|
+
* returned by {@link beginAuditCapture} when audit is disabled — the handler
|
|
429
|
+
* guards every write with `if (audit)`.
|
|
430
|
+
*/
|
|
431
|
+
interface AuditCaptureContext {
|
|
432
|
+
/** Outbound key id (set after auth). NEVER the key material. */
|
|
433
|
+
keyId?: string | null;
|
|
434
|
+
/** Resolved upstream model (set after route resolution). */
|
|
435
|
+
model?: string;
|
|
436
|
+
/** Upstream provider id (set after route resolution). */
|
|
437
|
+
provider?: string;
|
|
438
|
+
/** Sanitized error message (set on a relay/dispatch failure). */
|
|
439
|
+
error?: string;
|
|
440
|
+
/** Stash the raw request body for capture (a no-op unless `captureBodies`). */
|
|
441
|
+
setRequestBody(raw: string): void;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Begin audit capture for one request. Returns a mutable context the handler
|
|
445
|
+
* enriches, or `null` when audit is disabled (zero-cost path). The record is
|
|
446
|
+
* emitted on response `close` via the fire-and-forget sink.
|
|
447
|
+
*/
|
|
448
|
+
declare function beginAuditCapture(req: http.IncomingMessage, res: http.ServerResponse, now: number): AuditCaptureContext | null;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* billingCapture — the outbound server's post-response billing capture hook
|
|
452
|
+
* (billing-event-stream, design D1/D4).
|
|
453
|
+
*
|
|
454
|
+
* {@link beginBillingCapture} is called at the TOP of `handleOutboundRequest`. It
|
|
455
|
+
* reads the active billing config from the core slot; when billing is DISABLED it
|
|
456
|
+
* returns `null` immediately (one slot read — no assembly, no listener, zero
|
|
457
|
+
* regression). When enabled it registers a one-shot `res.close` listener that
|
|
458
|
+
* assembles a {@link BillingEvent} from the handler-filled context (keyId / model
|
|
459
|
+
* / provider / authMode), the response `statusCode`, and this request's METERED
|
|
460
|
+
* FACTS (token counts + the ALREADY-COMPUTED cost) — then hands it to the
|
|
461
|
+
* no-throw {@link publishBillingEvent}.
|
|
462
|
+
*
|
|
463
|
+
* COST REUSE (design D3 — no double pricing): the metered facts are read from the
|
|
464
|
+
* per-request usage stash the usage tap populates ({@link readAuditUsage} — the
|
|
465
|
+
* SAME stash `usage-events.jsonl` correlation uses, populated UNCONDITIONALLY by
|
|
466
|
+
* the taps, independent of whether request-audit is enabled). Token counts are
|
|
467
|
+
* stashed SYNCHRONOUSLY at `record()`; the cost lands on the recorder's deferred
|
|
468
|
+
* pricing tick (scheduled BEFORE the response closes). So the assembly is deferred
|
|
469
|
+
* one macrotask past `close` — by then the earlier-scheduled pricing tick has run
|
|
470
|
+
* and stashed the computed cost we REUSE (we never re-price).
|
|
471
|
+
*
|
|
472
|
+
* BILLABLE-ONLY: an event is emitted ONLY when the request produced usage (an
|
|
473
|
+
* upstream call was billed). Auth failures / rate-limit / model-restriction
|
|
474
|
+
* rejections return before dispatch, stash no usage, and emit NO billing event.
|
|
475
|
+
*
|
|
476
|
+
* @module @omnicross/core/outbound-api/billingCapture
|
|
477
|
+
*/
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The mutable context the request handler enriches as it progresses. `null` is
|
|
481
|
+
* returned by {@link beginBillingCapture} when billing is disabled — the handler
|
|
482
|
+
* guards every write with `if (billing)`.
|
|
483
|
+
*/
|
|
484
|
+
interface BillingCaptureContext {
|
|
485
|
+
/** Outbound key id (set after auth). NEVER the key material. */
|
|
486
|
+
keyId?: string | null;
|
|
487
|
+
/** Resolved upstream model (set after route resolution). */
|
|
488
|
+
model?: string;
|
|
489
|
+
/** Upstream provider id (set after route resolution). */
|
|
490
|
+
provider?: string;
|
|
491
|
+
/** Re-auth mode the request billed under (set after route resolution). */
|
|
492
|
+
authMode?: 'byo' | 'subscription';
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Deferral used to schedule the event assembly one macrotask past `close`, so the
|
|
496
|
+
* usage tap's earlier-scheduled pricing tick has stashed the cost we reuse.
|
|
497
|
+
* Injectable ONLY for deterministic tests; production uses a zero-delay timer.
|
|
498
|
+
*/
|
|
499
|
+
type BillingDefer = (fn: () => void) => void;
|
|
500
|
+
/**
|
|
501
|
+
* Begin billing capture for one request. Returns a mutable context the handler
|
|
502
|
+
* enriches, or `null` when billing is disabled (zero-cost path). The event is
|
|
503
|
+
* emitted on response `close` via the no-throw sink — but ONLY when the request
|
|
504
|
+
* produced usage (a billable request).
|
|
505
|
+
*/
|
|
506
|
+
declare function beginBillingCapture(_req: http.IncomingMessage, res: http.ServerResponse, now: number, defer?: BillingDefer): BillingCaptureContext | null;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* modelPrefixDispatch — pure model-name PREFIX classification for the `chat`
|
|
510
|
+
* endpoint's opt-in `dispatchMode: 'prefix'` (openai-chat-bridge #11, design D2).
|
|
511
|
+
*
|
|
512
|
+
* In prefix mode the requested model's leading vendor token selects a configured
|
|
513
|
+
* target from {@link ModelPrefixTargets} (`claude-*` → `claude`, `gpt-*` → `gpt`,
|
|
514
|
+
* `gemini-*` → `gemini`), so an operator can serve many upstreams from a single
|
|
515
|
+
* `/v1/chat/completions` without an explicit per-model list entry. The match is
|
|
516
|
+
* case-insensitive and anchored at the START of the id (a token prefix, not a
|
|
517
|
+
* substring — `my-gpt-thing` does NOT classify as `gpt`). This is a ROUTING
|
|
518
|
+
* convenience layered on top of the existing conversion machinery; it changes
|
|
519
|
+
* only which upstream a model resolves to, never how the body is translated.
|
|
520
|
+
*
|
|
521
|
+
* @module outbound-api/modelPrefixDispatch
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
/** The three core prefixes this dispatch vocabulary recognizes. */
|
|
525
|
+
type ModelPrefixKind = 'claude' | 'gpt' | 'gemini';
|
|
526
|
+
/**
|
|
527
|
+
* Classify a requested model id by its leading vendor token. Case-insensitive;
|
|
528
|
+
* anchored at the start (matches `<prefix>` exactly or `<prefix>-…`). Returns the
|
|
529
|
+
* matched {@link ModelPrefixKind}, or `null` when no known prefix applies (an
|
|
530
|
+
* empty/blank id also yields `null`).
|
|
531
|
+
*
|
|
532
|
+
* `gpt` also matches the OpenAI `o`-series reasoning ids (`o1`, `o3-mini`, …),
|
|
533
|
+
* which carry no `gpt` token but are the same OpenAI Chat-Completions family the
|
|
534
|
+
* `gpt` target serves.
|
|
535
|
+
*/
|
|
536
|
+
declare function classifyModelPrefix(model: string | undefined): ModelPrefixKind | null;
|
|
537
|
+
/**
|
|
538
|
+
* Resolve the configured target ref for a requested model under prefix dispatch.
|
|
539
|
+
* Returns the `ModelRef` for the matched prefix, or `null` when the model has no
|
|
540
|
+
* known prefix OR the matched prefix has no configured target (both are
|
|
541
|
+
* "unroutable" — the caller surfaces a clear per-request error).
|
|
542
|
+
*/
|
|
543
|
+
declare function resolvePrefixTarget(targets: ModelPrefixTargets | undefined, model: string | undefined): {
|
|
544
|
+
kind: ModelPrefixKind;
|
|
545
|
+
ref: ModelRef;
|
|
546
|
+
} | null;
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* outboundConcurrencyGate — per-`apiKeyId` concurrency queue for the outbound
|
|
550
|
+
* API server (queue/concurrency, design D-CORE-2).
|
|
551
|
+
*
|
|
552
|
+
* An outbound key over its concurrency ceiling should WAIT its turn rather than
|
|
553
|
+
* get a hard 429. This primitive is a per-key counting semaphore (`limit` =
|
|
554
|
+
* the key's `maxConcurrency`) fronting a bounded FIFO wait queue: within limit
|
|
555
|
+
* → grant; over limit but under `max(limit*factor, minQueueSize)` → enqueue;
|
|
556
|
+
* beyond that → reject queue-full. Waiters resolve in strict FIFO order and a
|
|
557
|
+
* waiter can be CANCELLED (the wire layer binds this to `res.close` so a client
|
|
558
|
+
* that disconnects mid-queue frees its spot). Release + cancel are idempotent
|
|
559
|
+
* (guarded by a per-acquisition `settled` flag) so the wire's `finally` +
|
|
560
|
+
* `res.once('close')` double-fire is safe — this directly avoids the CRS #1130
|
|
561
|
+
* slot leak.
|
|
562
|
+
*
|
|
563
|
+
* Memory-only, injected clock, resets on restart. `limit <= 0` means unlimited
|
|
564
|
+
* (the wire layer bypasses the gate entirely for such keys; a defensive call
|
|
565
|
+
* here still grants immediately without bound).
|
|
566
|
+
*
|
|
567
|
+
* NOTE ON THE ACQUIRE SHAPE: the design task sketched `acquire → Promise<{
|
|
568
|
+
* release, cancel }>`, but the wire layer must be able to CANCEL a still-pending
|
|
569
|
+
* wait (bind `res.once('close', cancel)` BEFORE the grant resolves). A bare
|
|
570
|
+
* promise only hands back its value on grant, so `acquire` instead returns a
|
|
571
|
+
* synchronous {@link GateAcquisition} handle exposing both `cancel()` and a
|
|
572
|
+
* `granted` promise; the granted {@link GateSlot} also carries `release` AND
|
|
573
|
+
* `cancel` (the same idempotent fns) so the sketched shape is still satisfied
|
|
574
|
+
* post-grant. (Deviation recorded in the change return notes.)
|
|
575
|
+
*
|
|
576
|
+
* @module outbound-api/outboundConcurrencyGate
|
|
577
|
+
*/
|
|
578
|
+
/** A granted concurrency slot. `release` frees it; `cancel` is a post-grant no-op. */
|
|
579
|
+
interface GateSlot {
|
|
580
|
+
release(): void;
|
|
581
|
+
cancel(): void;
|
|
582
|
+
}
|
|
583
|
+
/** The synchronous handle `acquire` returns (so a pending wait can be cancelled). */
|
|
584
|
+
interface GateAcquisition {
|
|
585
|
+
/** Resolves with a {@link GateSlot} on grant; rejects on queue-full/timeout/cancel. */
|
|
586
|
+
granted: Promise<GateSlot>;
|
|
587
|
+
/** Cancel a still-pending wait (idempotent; no-op once granted). */
|
|
588
|
+
cancel(): void;
|
|
589
|
+
}
|
|
590
|
+
/** Options for one `acquire`. */
|
|
591
|
+
interface GateAcquireOptions {
|
|
592
|
+
/** Per-key max queued = `max(limit*factor, minQueueSize)`. */
|
|
593
|
+
maxQueueSizeFactor: number;
|
|
594
|
+
/** Floor of the per-key max queued. */
|
|
595
|
+
minQueueSize: number;
|
|
596
|
+
/** Reject a queued waiter after this many ms (wire → 429). */
|
|
597
|
+
waitTimeoutMs: number;
|
|
598
|
+
}
|
|
599
|
+
/** A per-key snapshot entry (only keys with active slots or waiters). */
|
|
600
|
+
interface GateStatusEntry {
|
|
601
|
+
apiKeyId: string;
|
|
602
|
+
active: number;
|
|
603
|
+
waiting: number;
|
|
604
|
+
}
|
|
605
|
+
/** Rejection thrown when a key's wait queue is full at acquire time. */
|
|
606
|
+
declare class ConcurrencyQueueFullError extends Error {
|
|
607
|
+
readonly apiKeyId: string;
|
|
608
|
+
readonly maxQueueSize: number;
|
|
609
|
+
readonly code = "concurrency_queue_full";
|
|
610
|
+
constructor(apiKeyId: string, maxQueueSize: number);
|
|
611
|
+
}
|
|
612
|
+
/** Rejection thrown when a queued waiter exceeds `waitTimeoutMs`. */
|
|
613
|
+
declare class ConcurrencyWaitTimeoutError extends Error {
|
|
614
|
+
readonly apiKeyId: string;
|
|
615
|
+
readonly waitTimeoutMs: number;
|
|
616
|
+
readonly code = "concurrency_wait_timeout";
|
|
617
|
+
constructor(apiKeyId: string, waitTimeoutMs: number);
|
|
618
|
+
}
|
|
619
|
+
/** Rejection thrown when a still-pending wait is cancelled (client disconnect). */
|
|
620
|
+
declare class ConcurrencyWaitCancelledError extends Error {
|
|
621
|
+
readonly apiKeyId: string;
|
|
622
|
+
readonly code = "concurrency_wait_cancelled";
|
|
623
|
+
constructor(apiKeyId: string);
|
|
624
|
+
}
|
|
625
|
+
/** True for any rejection the gate produces on an acquire that never granted. */
|
|
626
|
+
declare function isConcurrencyRejection(err: unknown): err is ConcurrencyQueueFullError | ConcurrencyWaitTimeoutError | ConcurrencyWaitCancelledError;
|
|
627
|
+
/**
|
|
628
|
+
* Per-`apiKeyId` counting semaphore + bounded FIFO wait queue.
|
|
629
|
+
*/
|
|
630
|
+
declare class OutboundConcurrencyGate {
|
|
631
|
+
private readonly states;
|
|
632
|
+
private ensure;
|
|
633
|
+
/** Delete a key's state once it is fully idle (no active, no waiters). */
|
|
634
|
+
private gc;
|
|
635
|
+
/**
|
|
636
|
+
* Acquire a concurrency slot for `apiKeyId` under `limit`. Grants immediately
|
|
637
|
+
* when `active < limit`; else enqueues FIFO while under the per-key queue cap;
|
|
638
|
+
* else rejects queue-full. `limit <= 0` = unlimited (always granted).
|
|
639
|
+
*/
|
|
640
|
+
acquire(apiKeyId: string, limit: number, options: GateAcquireOptions): GateAcquisition;
|
|
641
|
+
/** Build an idempotent granted slot (`release` decrements; `cancel` is a no-op). */
|
|
642
|
+
private makeSlot;
|
|
643
|
+
/** After a release freed a slot, grant the FIFO head if it fits under its cap. */
|
|
644
|
+
private dispatchNext;
|
|
645
|
+
/** Snapshot for observability — only keys with active slots or waiters. */
|
|
646
|
+
getStatus(): GateStatusEntry[];
|
|
647
|
+
/** Drop all state + pending timers (tests / teardown). */
|
|
648
|
+
reset(): void;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* userMessageDetection — decide whether a parsed outbound request body is a
|
|
653
|
+
* REAL user-message turn (serialize it) vs a tool-loop continuation (bypass the
|
|
654
|
+
* serial queue) (queue/concurrency, design D-CORE-4).
|
|
655
|
+
*
|
|
656
|
+
* The serial queue protects a shared upstream account from looking like many
|
|
657
|
+
* concurrent humans. Only human-INITIATED turns should be serialized; a
|
|
658
|
+
* tool-loop turn (the client feeding a tool result back) must NOT be throttled
|
|
659
|
+
* or it stalls the agent. This module classifies the LAST turn per ingress
|
|
660
|
+
* format, aligned to claude-relay-service's `isUserMessageRequest` semantics but
|
|
661
|
+
* covering all four omnicross ingress formats.
|
|
662
|
+
*
|
|
663
|
+
* SAFETY BIAS: default `false` (bypass) on any shape it cannot POSITIVELY
|
|
664
|
+
* classify as a human turn. Under-serializing only costs the (opt-in, default-
|
|
665
|
+
* off) protection; over-serializing would stall real tool-loops.
|
|
666
|
+
*
|
|
667
|
+
* @module outbound-api/userMessageDetection
|
|
668
|
+
*/
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Decide whether `parsedBody` for `endpoint` is a real user-message turn (→
|
|
672
|
+
* serialize) vs a tool-loop / non-user turn (→ bypass). Defaults `false` on any
|
|
673
|
+
* unclassifiable shape.
|
|
674
|
+
*/
|
|
675
|
+
declare function isUserMessageRequest(endpoint: OutboundEndpoint, parsedBody: unknown): boolean;
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* userMessageSerialQueue — per-`providerId` user-message serial queue for the
|
|
679
|
+
* outbound API server (queue/concurrency, design D-CORE-1).
|
|
680
|
+
*
|
|
681
|
+
* A shared subscription account hit by concurrent user messages looks unlike a
|
|
682
|
+
* single human and trips upstream risk-control. This primitive serializes the
|
|
683
|
+
* REAL user-message turns for one upstream account (`providerId`): exactly one
|
|
684
|
+
* in-flight at a time, plus a `delayMs` minimum gap between one account's
|
|
685
|
+
* requests. Waiters resolve in strict FIFO order (correcting claude-relay-
|
|
686
|
+
* service's non-fair Redis polling); no jitter, no busy-poll — the residual
|
|
687
|
+
* `delayMs` gap is honored by a scheduled dispatch.
|
|
688
|
+
*
|
|
689
|
+
* Memory-only, resets on app restart (acceptable — the queue is a live
|
|
690
|
+
* throttle, not persisted state). The caller RELEASES on response start (not
|
|
691
|
+
* completion) so the next account request can begin while the prior response
|
|
692
|
+
* streams; the release timestamp seeds the `delayMs` gap for the next waiter.
|
|
693
|
+
* The wire layer (`omnicross-uqc-wire`) owns the `res` lifecycle; this module
|
|
694
|
+
* never touches HTTP.
|
|
695
|
+
*
|
|
696
|
+
* @module outbound-api/userMessageSerialQueue
|
|
697
|
+
*/
|
|
698
|
+
/** A held serial slot; `release()` frees it (idempotent). */
|
|
699
|
+
interface SerialSlot {
|
|
700
|
+
release(): void;
|
|
701
|
+
}
|
|
702
|
+
/** Options for one `acquire`. */
|
|
703
|
+
interface SerialAcquireOptions {
|
|
704
|
+
/** Reject the waiter after this many ms (wire → 503). */
|
|
705
|
+
waitTimeoutMs: number;
|
|
706
|
+
/** Minimum gap (ms) since this key's last release before the next grant. */
|
|
707
|
+
delayMs: number;
|
|
708
|
+
/** Clock reference for the immediate-grant decision (default `Date.now()`). */
|
|
709
|
+
now?: number;
|
|
710
|
+
}
|
|
711
|
+
/** A per-key snapshot entry (only keys with a held slot or non-empty queue). */
|
|
712
|
+
interface SerialQueueStatusEntry {
|
|
713
|
+
providerId: string;
|
|
714
|
+
holding: boolean;
|
|
715
|
+
waiting: number;
|
|
716
|
+
}
|
|
717
|
+
/** Rejection thrown when a serial waiter exceeds `waitTimeoutMs`. */
|
|
718
|
+
declare class SerialQueueTimeoutError extends Error {
|
|
719
|
+
readonly providerId: string;
|
|
720
|
+
readonly waitTimeoutMs: number;
|
|
721
|
+
readonly code = "serial_queue_timeout";
|
|
722
|
+
constructor(providerId: string, waitTimeoutMs: number);
|
|
723
|
+
}
|
|
724
|
+
/** True for a rejection produced by the serial queue wait-timeout. */
|
|
725
|
+
declare function isSerialQueueTimeout(err: unknown): err is SerialQueueTimeoutError;
|
|
726
|
+
/**
|
|
727
|
+
* Per-`providerId` single-slot mutex + `delayMs` spacing gate with a FIFO wait
|
|
728
|
+
* queue. `acquire` resolves when the key is free AND at least `delayMs` has
|
|
729
|
+
* elapsed since that key's last release.
|
|
730
|
+
*/
|
|
731
|
+
declare class UserMessageSerialQueue {
|
|
732
|
+
private readonly states;
|
|
733
|
+
private ensure;
|
|
734
|
+
/**
|
|
735
|
+
* Acquire the serial slot for `providerId`. Resolves immediately when the slot
|
|
736
|
+
* is free, no one is queued, and the `delayMs` gap since the last release has
|
|
737
|
+
* elapsed; otherwise enqueues FIFO and resolves in order (or rejects with a
|
|
738
|
+
* {@link SerialQueueTimeoutError} after `waitTimeoutMs`).
|
|
739
|
+
*/
|
|
740
|
+
acquire(providerId: string, options: SerialAcquireOptions): Promise<SerialSlot>;
|
|
741
|
+
/** Build an idempotent release for a granted slot. */
|
|
742
|
+
private makeSlot;
|
|
743
|
+
/**
|
|
744
|
+
* Grant the head waiter when the slot is free and the head's `delayMs` gap has
|
|
745
|
+
* elapsed; otherwise schedule a dispatch for the residual gap. No-op while the
|
|
746
|
+
* slot is held (release re-invokes this) or the queue is empty.
|
|
747
|
+
*/
|
|
748
|
+
private maybeDispatch;
|
|
749
|
+
/** Hand the slot to the FIFO head (if free and someone is waiting). */
|
|
750
|
+
private grantHead;
|
|
751
|
+
/** Snapshot for observability — only keys holding a slot or with waiters. */
|
|
752
|
+
getStatus(): SerialQueueStatusEntry[];
|
|
753
|
+
/** Drop all state + pending timers (tests / teardown). */
|
|
754
|
+
reset(): void;
|
|
755
|
+
}
|
|
756
|
+
|
|
122
757
|
/**
|
|
123
758
|
* outboundApiKeyAuth — named-key generation, hashing, and verification for the
|
|
124
759
|
* outbound API server (`outbound-api-server`).
|
|
@@ -131,6 +766,13 @@ declare function mergeServerConfig(current: OutboundApiServerConfig, patch: Part
|
|
|
131
766
|
* @module outbound-api/outboundApiKeyAuth
|
|
132
767
|
*/
|
|
133
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Produce `count` UNBIASED base62 chars (t2). Uses rejection sampling — bytes in
|
|
771
|
+
* `[248, 256)` are discarded so the remaining `[0, 248)` map uniformly onto the
|
|
772
|
+
* 62 alphabet (each char equally likely), eliminating the `byte % 62` modulo
|
|
773
|
+
* bias. Draws fresh random bytes in batches until enough chars are accepted.
|
|
774
|
+
*/
|
|
775
|
+
declare function randomBase62(count: number): string;
|
|
134
776
|
/** Hash a presented/generated secret (sha256 hex). */
|
|
135
777
|
declare function hashKey(secret: string): string;
|
|
136
778
|
/**
|
|
@@ -138,18 +780,90 @@ declare function hashKey(secret: string): string;
|
|
|
138
780
|
* prefix are stored.
|
|
139
781
|
*/
|
|
140
782
|
declare function createNamedKey(db: OutboundKeyDb, name: string): Promise<OutboundApiKeyCreated>;
|
|
141
|
-
/**
|
|
783
|
+
/**
|
|
784
|
+
* A verified key + the enforcement inputs carried from the row so the wire layer
|
|
785
|
+
* runs its policy checks WITHOUT a second DB read (mirrors `maxConcurrency`).
|
|
786
|
+
*/
|
|
142
787
|
interface VerifiedKey {
|
|
143
788
|
id: string;
|
|
789
|
+
/**
|
|
790
|
+
* The key's per-key concurrency ceiling, carried from the row so the wire
|
|
791
|
+
* layer keys the concurrency gate without a second DB read. Absent/`0` =
|
|
792
|
+
* unlimited (gate bypassed).
|
|
793
|
+
*/
|
|
794
|
+
maxConcurrency?: number;
|
|
795
|
+
/**
|
|
796
|
+
* Per-key USD cost limits (outbound-key-policy). Absent when the key has no
|
|
797
|
+
* cost cap → the wire layer skips the cost-quota check entirely.
|
|
798
|
+
*/
|
|
799
|
+
costLimits?: KeyCostLimits;
|
|
800
|
+
/**
|
|
801
|
+
* Per-key rate-limit override (outbound-key-policy). Absent when the key has no
|
|
802
|
+
* rate config → the limiter uses its default 60/60s window (byte-identical).
|
|
803
|
+
*/
|
|
804
|
+
rateLimit?: {
|
|
805
|
+
maxRequests?: number;
|
|
806
|
+
windowMs?: number;
|
|
807
|
+
};
|
|
808
|
+
/**
|
|
809
|
+
* Per-key model restriction (outbound-key-policy #6). Populated ONLY when the
|
|
810
|
+
* row has `enableModelRestriction === true` → the wire layer's presence check
|
|
811
|
+
* is the zero-regression gate. Absent ⇒ NO model check runs for this key.
|
|
812
|
+
*/
|
|
813
|
+
modelRestriction?: ModelRestriction;
|
|
144
814
|
}
|
|
815
|
+
/** The reason-bearing verify outcome (design D2). */
|
|
816
|
+
type KeyVerification = {
|
|
817
|
+
status: 'ok';
|
|
818
|
+
key: VerifiedKey;
|
|
819
|
+
} | {
|
|
820
|
+
status: 'invalid';
|
|
821
|
+
} | {
|
|
822
|
+
status: 'expired';
|
|
823
|
+
};
|
|
824
|
+
/**
|
|
825
|
+
* Verify a presented key against the DB, returning a REASON (design D2) so the
|
|
826
|
+
* wire layer can emit the right status + a clear body. Matches by hash where the
|
|
827
|
+
* stored row is enabled AND not revoked. On a valid, non-expired key: bumps
|
|
828
|
+
* `lastUsedAt` (best-effort) and, for an activation-mode key on its FIRST use,
|
|
829
|
+
* stamps `activatedAt` once (best-effort). A policy-less enabled key resolves to
|
|
830
|
+
* `{ status:'ok', key:{ id } }` — byte-identical to the pre-policy result.
|
|
831
|
+
*/
|
|
832
|
+
declare function verifyKey(db: OutboundKeyDb, presentedKey: string | undefined, now?: number): Promise<KeyVerification>;
|
|
145
833
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* `null` on any miss / disabled / revoked key.
|
|
834
|
+
* Thin id-or-null wrapper over {@link verifyKey} for callers not ready for the
|
|
835
|
+
* reason-bearing form: returns the `VerifiedKey` on success, `null` on any
|
|
836
|
+
* invalid/expired key (the exact pre-policy contract).
|
|
150
837
|
*/
|
|
151
838
|
declare function verifyPresentedKey(db: OutboundKeyDb, presentedKey: string | undefined): Promise<VerifiedKey | null>;
|
|
152
839
|
|
|
840
|
+
/**
|
|
841
|
+
* keyedMutex — a tiny per-key async mutex (voucher-redemption #9, MJ1 fix).
|
|
842
|
+
*
|
|
843
|
+
* Serializes async critical sections that share a key so they run
|
|
844
|
+
* one-after-another (each observing the previous one's committed effects),
|
|
845
|
+
* mirroring the subscription `RefreshMutex` "one in-flight op per key" pattern.
|
|
846
|
+
* Different keys never block each other.
|
|
847
|
+
*
|
|
848
|
+
* Used by the voucher redeem path to serialize a key's redemptions: two redeems
|
|
849
|
+
* for the SAME key run sequentially, so each reads the other's applied result and
|
|
850
|
+
* a RELATIVE grant increment accumulates instead of clobbering a shared snapshot.
|
|
851
|
+
*
|
|
852
|
+
* @module outbound-api/keyedMutex
|
|
853
|
+
*/
|
|
854
|
+
/** A per-key FIFO async mutex. In-memory; process-local. */
|
|
855
|
+
declare class KeyedMutex {
|
|
856
|
+
/** key → the tail of the pending-op chain (resolves when the last op frees). */
|
|
857
|
+
private readonly tails;
|
|
858
|
+
/**
|
|
859
|
+
* Run `fn` exclusively for `key`: it starts only after every previously
|
|
860
|
+
* enqueued op for the SAME key has settled, and the next waiter starts only
|
|
861
|
+
* after `fn` settles. Returns `fn`'s result (or rejection). Never lets one op's
|
|
862
|
+
* failure wedge the queue (waiters proceed regardless).
|
|
863
|
+
*/
|
|
864
|
+
runExclusive<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
865
|
+
}
|
|
866
|
+
|
|
153
867
|
/**
|
|
154
868
|
* outboundRateLimiter — per-API-key in-memory sliding-window rate limiter for
|
|
155
869
|
* the outbound API server (`outbound-api-server`, design D6).
|
|
@@ -184,29 +898,77 @@ declare class OutboundRateLimiter {
|
|
|
184
898
|
* Record a request for `apiKeyId` and decide whether it is allowed. Prunes
|
|
185
899
|
* timestamps older than the window first; when allowed, the request's
|
|
186
900
|
* timestamp is appended.
|
|
901
|
+
*
|
|
902
|
+
* `override` (outbound-key-policy) supplies a PER-KEY window/max for this
|
|
903
|
+
* bucket, superseding the instance defaults for THAT key. Absent ⇒ the
|
|
904
|
+
* instance default 60/60s (byte-identical to before this change). An effective
|
|
905
|
+
* `maxRequests` of `0` means UNLIMITED — the request is allowed and NOT
|
|
906
|
+
* recorded (no bucket growth).
|
|
187
907
|
*/
|
|
188
|
-
check(apiKeyId: string, now?: number): RateLimitDecision;
|
|
908
|
+
check(apiKeyId: string, now?: number, override?: RateLimiterOptions): RateLimitDecision;
|
|
189
909
|
/** Drop all recorded state (tests / teardown). */
|
|
190
910
|
reset(): void;
|
|
191
911
|
}
|
|
192
912
|
|
|
193
913
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
914
|
+
* voucherRedeem — the key-authenticated `POST <base>/redeem` handler
|
|
915
|
+
* (voucher-redemption #9, design D1/D4/D5/D6; MJ1/M2/MJ2/M3 fix).
|
|
916
|
+
*
|
|
917
|
+
* A key presents its own outbound secret (already verified by the caller) plus a
|
|
918
|
+
* card `{ code }`; the card's value applies to THAT key. The critical section is
|
|
919
|
+
* SERIALIZED PER KEY (an async `KeyedMutex`) so two redemptions for the same key
|
|
920
|
+
* run one-after-another — the fix for concurrent value loss (MJ1). Each redeem:
|
|
921
|
+
* 1. RECONCILE any stranded (`redeemed && grantApplied !== true`) card for this
|
|
922
|
+
* key FIRST — re-applying its recorded ABSOLUTE target and marking it — so a
|
|
923
|
+
* prior card's grant settles BEFORE this redeem computes anything (M2: the
|
|
924
|
+
* recorded absolute is therefore never stale).
|
|
925
|
+
* 2. hash the code → look up → require `unredeemed`.
|
|
926
|
+
* 3. compute the grant from the CURRENT policy read inside the mutex — this is
|
|
927
|
+
* the intended final ABSOLUTE key value (`min(current + credit, cap)` /
|
|
928
|
+
* `min(current + days, now + capDays)`). Because the mutex serialized step 1,
|
|
929
|
+
* "current" already includes every earlier card, so concurrent DIFFERENT
|
|
930
|
+
* cards accumulate correctly.
|
|
931
|
+
* 4. ATOMIC CAS flip `unredeemed → redeemed` recording that absolute +
|
|
932
|
+
* `grantApplied = false` — the single-use guard.
|
|
933
|
+
* 5. apply `setPolicy(key, <recorded absolute>)`; on success set
|
|
934
|
+
* `grantApplied = true`.
|
|
935
|
+
*
|
|
936
|
+
* Crash-safety WITHOUT double-credit (MJ2): the apply target is the recorded
|
|
937
|
+
* ABSOLUTE, applied idempotently on BOTH the first pass AND the reconcile — so a
|
|
938
|
+
* crash between the `setPolicy` and the `grantApplied` mark just re-applies the
|
|
939
|
+
* SAME absolute (a no-op), never a second credit. If the apply FAILS on the first
|
|
940
|
+
* pass (key revoked mid-redeem, M3) the flip is REVERTED and the holder gets an
|
|
941
|
+
* error. Redeem attempts are rate-limited (D6). The response reveals ONLY this
|
|
942
|
+
* key's own balance.
|
|
943
|
+
*
|
|
944
|
+
* @module outbound-api/voucherRedeem
|
|
945
|
+
*/
|
|
946
|
+
|
|
947
|
+
/** True for `POST <base>/redeem` (or `/v1/redeem`) — the redeem endpoint. */
|
|
948
|
+
declare function isRedeemRequest(method: string | undefined, url: string | undefined): boolean;
|
|
949
|
+
/**
|
|
950
|
+
* Handle a redeem request. `deps` carries the voucher store + the key DB; the key
|
|
951
|
+
* is ALREADY verified by the router (`verifiedKeyId` / `presentedKey`). Gated on
|
|
952
|
+
* `voucherEnabled` (disabled ⇒ inert). `redeemLimiter` throttles attempts (D6);
|
|
953
|
+
* `redeemMutex` serializes a key's redemptions (MJ1 fix).
|
|
954
|
+
*/
|
|
955
|
+
declare function handleVoucherRedeem(req: http.IncomingMessage, res: http.ServerResponse, deps: OutboundApiDeps, voucherEnabled: boolean, redeemLimiter: OutboundRateLimiter, verifiedKeyId: string, presentedKey: string, now: number, redeemMutex?: KeyedMutex): Promise<void>;
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* roleDetection — classify an outbound request's ROLE (background / default) so
|
|
959
|
+
* the route resolver can pick the endpoint's model for that role
|
|
196
960
|
* (`outbound-api-server`, design D2).
|
|
197
961
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
962
|
+
* Applies to the role-based endpoints (`chat`/`gemini`) only; the kind-mapped
|
|
963
|
+
* endpoints (`messages`/`responses`) classify by model KIND in `kindDetection`.
|
|
964
|
+
*
|
|
965
|
+
* Precedence: background > default.
|
|
202
966
|
* - background — the requested model id is in the endpoint's optional
|
|
203
967
|
* background-model-id override list (human decision after the
|
|
204
968
|
* proposal), OR the registry small/haiku-class name signal
|
|
205
969
|
* matches (Claude Code's haiku probe sends exactly this).
|
|
206
970
|
* - default — everything else.
|
|
207
971
|
*
|
|
208
|
-
* Each ingress's body shape is handled here in ONE place.
|
|
209
|
-
*
|
|
210
972
|
* @module outbound-api/roleDetection
|
|
211
973
|
*/
|
|
212
974
|
|
|
@@ -214,8 +976,7 @@ declare class OutboundRateLimiter {
|
|
|
214
976
|
* Detect the request's role. `backgroundModelIds` is the endpoint's optional
|
|
215
977
|
* override list: when an incoming requested model id matches an entry there, the
|
|
216
978
|
* request is BACKGROUND regardless of the name signal; otherwise the registry
|
|
217
|
-
* small/haiku-class name signal is the baseline. Precedence
|
|
218
|
-
* > default.
|
|
979
|
+
* small/haiku-class name signal is the baseline. Precedence background > default.
|
|
219
980
|
*/
|
|
220
981
|
declare function detectRequestRole(ingressFormat: IngressFormat, body: Record<string, unknown>, options?: {
|
|
221
982
|
backgroundModelIds?: string[];
|
|
@@ -245,4 +1006,4 @@ declare function getOutboundApiServer(deps?: OutboundApiDeps, onPortChange?: (po
|
|
|
245
1006
|
/** Reset the singleton (tests / teardown only). */
|
|
246
1007
|
declare function __resetOutboundApiServerForTests(): void;
|
|
247
1008
|
|
|
248
|
-
export { type ApiServerSettingsStore, type ApplyConfigInput, DEFAULT_OUTBOUND_PORT, EndpointRoutingConfig, OUTBOUND_API_SERVER_CONFIG_KEY, OutboundApiDeps, OutboundApiKeyCreated, OutboundApiServer, OutboundApiServerConfig, OutboundApiServerStatus, OutboundFormatUrls, OutboundKeyDb, OutboundRateLimiter, RequestRole, __resetOutboundApiServerForTests, createNamedKey, defaultServerConfig, detectRequestRole, endpointToIngressFormat, formatUrls, getOutboundApiServer, hashKey, loadServerConfig, mergeServerConfig, normalizeServerConfig, saveServerConfig, verifyPresentedKey };
|
|
1009
|
+
export { AUDIT_REDACTED, AccountProbeConfig, type ApiServerSettingsStore, type ApplyConfigInput, type AuditCaptureContext, type BillingCaptureContext, ConcurrencyQueueConfig, ConcurrencyQueueFullError, ConcurrencyWaitCancelledError, ConcurrencyWaitTimeoutError, DEFAULT_ACCOUNT_PROBE, DEFAULT_CONCURRENCY_QUEUE, DEFAULT_FINGERPRINT, DEFAULT_OUTBOUND_PORT, DEFAULT_USER_MESSAGE_QUEUE, type EndpointModelConfigError, EndpointRoutingConfig, FingerprintConfig, type GateAcquireOptions, type GateAcquisition, type GateSlot, type GateStatusEntry, KeyCostLimits, type KeyVerification, KeyedMutex, KindMappedEndpoint, ModelKind, type ModelPrefixKind, ModelPrefixTargets, OUTBOUND_API_SERVER_CONFIG_KEY, OutboundApiConfigError, OutboundApiDeps, OutboundApiKeyCreated, OutboundApiServer, OutboundApiServerConfig, OutboundApiServerStatus, OutboundConcurrencyGate, OutboundEndpoint, OutboundFormatUrls, OutboundKeyDb, OutboundProxyConfig, OutboundRateLimiter, RequestRole, type SerialAcquireOptions, type SerialQueueStatusEntry, SerialQueueTimeoutError, type SerialSlot, UserMessageQueueConfig, UserMessageSerialQueue, type VerifiedKey, __resetOutboundApiServerForTests, beginAuditCapture, beginBillingCapture, classifyModelPrefix, createNamedKey, defaultServerConfig, detectModelKind, detectRequestRole, endpointToIngressFormat, formatUrls, getOutboundApiServer, handleVoucherRedeem, hashKey, isConcurrencyRejection, isKindMappedEndpoint, isRedeemRequest, isSerialQueueTimeout, isUserMessageRequest, loadServerConfig, mergeServerConfig, modelKindsForEndpoint, normalizeAccountProbe, normalizeAudit, normalizeBilling, normalizeFingerprint, normalizePrefixTargets, normalizeProxyConfig, normalizeProxySegment, normalizeQueueSegments, normalizeServerConfig, normalizeVoucher, normalizeWebhookDestination, normalizeWebhookSegment, randomBase62, redactAuditText, resolvePrefixTarget, saveServerConfig, validateEndpointModelConfig, validateServerModelConfig, verifyKey, verifyPresentedKey };
|