@elsium-ai/core 0.2.1 → 0.2.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/README.md +1117 -19
- package/dist/config.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/stream.d.ts +0 -1
- package/dist/stream.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @elsium-ai/core
|
|
2
2
|
|
|
3
|
-
Core types,
|
|
3
|
+
Core types, errors, result pattern, streaming, and infrastructure utilities for [ElsiumAI](https://github.com/elsium-ai/elsium-ai).
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@elsium-ai/core)
|
|
6
6
|
[](https://github.com/elsium-ai/elsium-ai/blob/main/LICENSE)
|
|
@@ -13,38 +13,1136 @@ npm install @elsium-ai/core
|
|
|
13
13
|
|
|
14
14
|
## What's Inside
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
| Category | Exports |
|
|
17
|
+
|---|---|
|
|
18
|
+
| **Types** | `Role`, `TextContent`, `ImageContent`, `ContentPart`, `ToolCall`, `ToolResult`, `Message`, `TokenUsage`, `CostBreakdown`, `StopReason`, `LLMResponse`, `StreamEvent`, `StreamCheckpoint`, `XRayData`, `ProviderConfig`, `CompletionRequest`, `ToolDefinition`, `MiddlewareContext`, `MiddlewareNext`, `Middleware` |
|
|
19
|
+
| **Errors** | `ElsiumError`, `ErrorCode`, `ErrorDetails` |
|
|
20
|
+
| **Result** | `Result`, `Ok`, `Err`, `ok()`, `err()`, `isOk()`, `isErr()`, `unwrap()`, `unwrapOr()`, `tryCatch()`, `tryCatchSync()` |
|
|
21
|
+
| **Stream** | `ElsiumStream`, `createStream()`, `StreamTransformer`, `ResilientStreamOptions` |
|
|
22
|
+
| **Logger** | `createLogger()`, `Logger`, `LogLevel`, `LogEntry`, `LoggerOptions` |
|
|
23
|
+
| **Config** | `env()`, `envNumber()`, `envBool()` |
|
|
24
|
+
| **Utilities** | `generateId()`, `generateTraceId()`, `extractText()`, `sleep()`, `retry()` |
|
|
25
|
+
| **Circuit Breaker** | `createCircuitBreaker()`, `CircuitBreakerConfig`, `CircuitBreaker`, `CircuitState` |
|
|
26
|
+
| **Request Dedup** | `createDedup()`, `dedupMiddleware()`, `DedupConfig`, `Dedup` |
|
|
27
|
+
| **Policy Engine** | `createPolicySet()`, `policyMiddleware()`, `modelAccessPolicy()`, `tokenLimitPolicy()`, `costLimitPolicy()`, `contentPolicy()`, `PolicyDecision`, `PolicyResult`, `PolicyContext`, `PolicyRule`, `PolicyConfig`, `PolicySet` |
|
|
28
|
+
| **Shutdown** | `createShutdownManager()`, `ShutdownConfig`, `ShutdownManager` |
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
---
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
## Types
|
|
33
|
+
|
|
34
|
+
All type exports are interfaces and type aliases — no runtime cost.
|
|
35
|
+
|
|
36
|
+
### Role
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
type Role = 'system' | 'user' | 'assistant' | 'tool'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### TextContent
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
interface TextContent {
|
|
46
|
+
type: 'text'
|
|
47
|
+
text: string
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### ImageContent
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
interface ImageContent {
|
|
55
|
+
type: 'image'
|
|
56
|
+
source:
|
|
57
|
+
| { type: 'base64'; mediaType: string; data: string }
|
|
58
|
+
| { type: 'url'; url: string }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### ContentPart
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type ContentPart = TextContent | ImageContent
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### ToolCall
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
interface ToolCall {
|
|
72
|
+
id: string
|
|
73
|
+
name: string
|
|
74
|
+
arguments: Record<string, unknown>
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### ToolResult
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
interface ToolResult {
|
|
82
|
+
toolCallId: string
|
|
83
|
+
content: string
|
|
84
|
+
isError?: boolean
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Message
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
interface Message {
|
|
92
|
+
role: Role
|
|
93
|
+
content: string | ContentPart[]
|
|
94
|
+
name?: string
|
|
95
|
+
toolCalls?: ToolCall[]
|
|
96
|
+
toolResults?: ToolResult[]
|
|
97
|
+
metadata?: Record<string, unknown>
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### TokenUsage
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
interface TokenUsage {
|
|
105
|
+
inputTokens: number
|
|
106
|
+
outputTokens: number
|
|
107
|
+
totalTokens: number
|
|
108
|
+
cacheReadTokens?: number
|
|
109
|
+
cacheWriteTokens?: number
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### CostBreakdown
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
interface CostBreakdown {
|
|
117
|
+
inputCost: number
|
|
118
|
+
outputCost: number
|
|
119
|
+
totalCost: number
|
|
120
|
+
currency: 'USD'
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### StopReason
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
type StopReason = 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### LLMResponse
|
|
131
|
+
|
|
132
|
+
The unified response shape returned by all providers after a completion.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
interface LLMResponse {
|
|
136
|
+
id: string
|
|
137
|
+
message: Message
|
|
138
|
+
usage: TokenUsage
|
|
139
|
+
cost: CostBreakdown
|
|
140
|
+
model: string
|
|
141
|
+
provider: string
|
|
142
|
+
stopReason: StopReason
|
|
143
|
+
latencyMs: number
|
|
144
|
+
traceId: string
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### StreamEvent
|
|
149
|
+
|
|
150
|
+
A discriminated union of all events emitted during streaming.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
type StreamEvent =
|
|
154
|
+
| { type: 'text_delta'; text: string }
|
|
155
|
+
| { type: 'tool_call_start'; toolCall: { id: string; name: string } }
|
|
156
|
+
| { type: 'tool_call_delta'; toolCallId: string; arguments: string }
|
|
157
|
+
| { type: 'tool_call_end'; toolCallId: string }
|
|
158
|
+
| { type: 'message_start'; id: string; model: string }
|
|
159
|
+
| { type: 'message_end'; usage: TokenUsage; stopReason: StopReason }
|
|
160
|
+
| { type: 'error'; error: Error }
|
|
161
|
+
| { type: 'checkpoint'; checkpoint: StreamCheckpoint }
|
|
162
|
+
| { type: 'recovery'; partialText: string; error: Error }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### StreamCheckpoint
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
interface StreamCheckpoint {
|
|
169
|
+
id: string
|
|
170
|
+
timestamp: number
|
|
171
|
+
text: string
|
|
172
|
+
tokensSoFar: number
|
|
173
|
+
eventIndex: number
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### XRayData
|
|
178
|
+
|
|
179
|
+
Full request/response trace data for observability.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
interface XRayData {
|
|
183
|
+
traceId: string
|
|
184
|
+
timestamp: number
|
|
185
|
+
provider: string
|
|
186
|
+
model: string
|
|
187
|
+
latencyMs: number
|
|
188
|
+
request: {
|
|
189
|
+
url: string
|
|
190
|
+
method: string
|
|
191
|
+
headers: Record<string, string>
|
|
192
|
+
body: Record<string, unknown>
|
|
193
|
+
}
|
|
194
|
+
response: {
|
|
195
|
+
status: number
|
|
196
|
+
headers: Record<string, string>
|
|
197
|
+
body: Record<string, unknown>
|
|
198
|
+
}
|
|
199
|
+
usage: TokenUsage
|
|
200
|
+
cost: CostBreakdown
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### ProviderConfig
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
interface ProviderConfig {
|
|
208
|
+
apiKey: string
|
|
209
|
+
baseUrl?: string
|
|
210
|
+
timeout?: number
|
|
211
|
+
maxRetries?: number
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### CompletionRequest
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
interface CompletionRequest {
|
|
219
|
+
messages: Message[]
|
|
220
|
+
model?: string
|
|
221
|
+
system?: string
|
|
222
|
+
maxTokens?: number
|
|
223
|
+
temperature?: number
|
|
224
|
+
seed?: number
|
|
225
|
+
topP?: number
|
|
226
|
+
stopSequences?: string[]
|
|
227
|
+
tools?: ToolDefinition[]
|
|
228
|
+
schema?: z.ZodType
|
|
229
|
+
stream?: boolean
|
|
230
|
+
metadata?: Record<string, unknown>
|
|
231
|
+
signal?: AbortSignal
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### ToolDefinition
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
interface ToolDefinition {
|
|
239
|
+
name: string
|
|
240
|
+
description: string
|
|
241
|
+
inputSchema: Record<string, unknown>
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Middleware types
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
interface MiddlewareContext {
|
|
249
|
+
request: CompletionRequest
|
|
250
|
+
provider: string
|
|
251
|
+
model: string
|
|
252
|
+
traceId: string
|
|
253
|
+
startTime: number
|
|
254
|
+
metadata: Record<string, unknown>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
type MiddlewareNext = (ctx: MiddlewareContext) => Promise<LLMResponse>
|
|
258
|
+
|
|
259
|
+
type Middleware = (ctx: MiddlewareContext, next: MiddlewareNext) => Promise<LLMResponse>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Errors
|
|
265
|
+
|
|
266
|
+
### ErrorCode
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
type ErrorCode =
|
|
270
|
+
| 'PROVIDER_ERROR'
|
|
271
|
+
| 'RATE_LIMIT'
|
|
272
|
+
| 'AUTH_ERROR'
|
|
273
|
+
| 'INVALID_REQUEST'
|
|
274
|
+
| 'TIMEOUT'
|
|
275
|
+
| 'NETWORK_ERROR'
|
|
276
|
+
| 'PARSE_ERROR'
|
|
277
|
+
| 'VALIDATION_ERROR'
|
|
278
|
+
| 'TOOL_ERROR'
|
|
279
|
+
| 'BUDGET_EXCEEDED'
|
|
280
|
+
| 'MAX_ITERATIONS'
|
|
281
|
+
| 'STREAM_ERROR'
|
|
282
|
+
| 'CONFIG_ERROR'
|
|
283
|
+
| 'UNKNOWN'
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### ErrorDetails
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
interface ErrorDetails {
|
|
290
|
+
code: ErrorCode
|
|
291
|
+
message: string
|
|
292
|
+
provider?: string
|
|
293
|
+
model?: string
|
|
294
|
+
statusCode?: number
|
|
295
|
+
retryable: boolean
|
|
296
|
+
retryAfterMs?: number
|
|
297
|
+
cause?: Error
|
|
298
|
+
metadata?: Record<string, unknown>
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### ElsiumError
|
|
303
|
+
|
|
304
|
+
Structured error class used throughout the framework. Extends `Error` with machine-readable fields for error handling, retries, and observability.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
class ElsiumError extends Error {
|
|
308
|
+
readonly code: ErrorCode
|
|
309
|
+
readonly provider?: string
|
|
310
|
+
readonly model?: string
|
|
311
|
+
readonly statusCode?: number
|
|
312
|
+
readonly retryable: boolean
|
|
313
|
+
readonly retryAfterMs?: number
|
|
314
|
+
readonly cause?: Error
|
|
315
|
+
readonly metadata?: Record<string, unknown>
|
|
316
|
+
|
|
317
|
+
constructor(details: ErrorDetails)
|
|
318
|
+
toJSON(): Record<string, unknown>
|
|
319
|
+
|
|
320
|
+
static providerError(message: string, opts: {
|
|
321
|
+
provider: string
|
|
322
|
+
statusCode?: number
|
|
323
|
+
retryable?: boolean
|
|
324
|
+
cause?: Error
|
|
325
|
+
}): ElsiumError
|
|
326
|
+
|
|
327
|
+
static rateLimit(provider: string, retryAfterMs?: number): ElsiumError
|
|
328
|
+
static authError(provider: string): ElsiumError
|
|
329
|
+
static timeout(provider: string, timeoutMs: number): ElsiumError
|
|
330
|
+
static validation(message: string, metadata?: Record<string, unknown>): ElsiumError
|
|
331
|
+
static budgetExceeded(spent: number, budget: number): ElsiumError
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### Static factory methods
|
|
336
|
+
|
|
337
|
+
**`ElsiumError.providerError(message, opts)`** — Generic provider failure.
|
|
338
|
+
|
|
339
|
+
| Parameter | Type | Description |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| `message` | `string` | Error description |
|
|
342
|
+
| `opts.provider` | `string` | Provider name |
|
|
343
|
+
| `opts.statusCode` | `number?` | HTTP status code |
|
|
344
|
+
| `opts.retryable` | `boolean?` | Whether to retry (default `false`) |
|
|
345
|
+
| `opts.cause` | `Error?` | Underlying error |
|
|
346
|
+
|
|
347
|
+
**`ElsiumError.rateLimit(provider, retryAfterMs?)`** — Rate limit (429). Always retryable.
|
|
348
|
+
|
|
349
|
+
**`ElsiumError.authError(provider)`** — Authentication failure (401). Not retryable.
|
|
350
|
+
|
|
351
|
+
**`ElsiumError.timeout(provider, timeoutMs)`** — Request timeout. Retryable.
|
|
352
|
+
|
|
353
|
+
**`ElsiumError.validation(message, metadata?)`** — Validation failure. Not retryable.
|
|
354
|
+
|
|
355
|
+
**`ElsiumError.budgetExceeded(spent, budget)`** — Token/cost budget exceeded. Not retryable.
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { ElsiumError } from '@elsium-ai/core'
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
await callProvider()
|
|
362
|
+
} catch (e) {
|
|
363
|
+
if (e instanceof ElsiumError && e.retryable) {
|
|
364
|
+
// safe to retry
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Create specific errors
|
|
369
|
+
const err = ElsiumError.rateLimit('anthropic', 5000)
|
|
370
|
+
console.log(err.code) // 'RATE_LIMIT'
|
|
371
|
+
console.log(err.retryAfterMs) // 5000
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Result
|
|
377
|
+
|
|
378
|
+
A type-safe Result pattern for representing success/failure without exceptions.
|
|
379
|
+
|
|
380
|
+
### Types
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
type Result<T, E = Error> = Ok<T> | Err<E>
|
|
384
|
+
|
|
385
|
+
interface Ok<T> {
|
|
386
|
+
readonly ok: true
|
|
387
|
+
readonly value: T
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
interface Err<E> {
|
|
391
|
+
readonly ok: false
|
|
392
|
+
readonly error: E
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### ok()
|
|
397
|
+
|
|
398
|
+
Wraps a value in a success result.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
function ok<T>(value: T): Ok<T>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### err()
|
|
405
|
+
|
|
406
|
+
Wraps an error in a failure result.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
function err<E>(error: E): Err<E>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### isOk()
|
|
413
|
+
|
|
414
|
+
Type guard that narrows a `Result` to `Ok`.
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
function isOk<T, E>(result: Result<T, E>): result is Ok<T>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### isErr()
|
|
421
|
+
|
|
422
|
+
Type guard that narrows a `Result` to `Err`.
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
function isErr<T, E>(result: Result<T, E>): result is Err<E>
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### unwrap()
|
|
429
|
+
|
|
430
|
+
Extracts the value from an `Ok`, or throws the error from an `Err`.
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
function unwrap<T, E>(result: Result<T, E>): T
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### unwrapOr()
|
|
437
|
+
|
|
438
|
+
Extracts the value from an `Ok`, or returns the fallback for an `Err`.
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
function unwrapOr<T, E>(result: Result<T, E>, fallback: T): T
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### tryCatch()
|
|
445
|
+
|
|
446
|
+
Wraps an async function in a `Result`. Caught errors are normalized to `Error`.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
function tryCatch<T>(fn: () => Promise<T>): Promise<Result<T, Error>>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### tryCatchSync()
|
|
453
|
+
|
|
454
|
+
Synchronous version of `tryCatch`.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
function tryCatchSync<T>(fn: () => T): Result<T, Error>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import { ok, err, isOk, unwrap, unwrapOr, tryCatch } from '@elsium-ai/core'
|
|
462
|
+
|
|
463
|
+
// Manual construction
|
|
464
|
+
const success = ok(42)
|
|
465
|
+
const failure = err(new Error('boom'))
|
|
466
|
+
|
|
467
|
+
if (isOk(success)) {
|
|
468
|
+
console.log(success.value) // 42
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Safe unwrap with fallback
|
|
472
|
+
unwrapOr(failure, 0) // 0
|
|
473
|
+
|
|
474
|
+
// Wrap async operations
|
|
475
|
+
const result = await tryCatch(() => fetch('/api/data').then(r => r.json()))
|
|
476
|
+
if (isOk(result)) {
|
|
477
|
+
console.log(result.value)
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Stream
|
|
484
|
+
|
|
485
|
+
### StreamTransformer
|
|
486
|
+
|
|
487
|
+
A function that transforms a stream of events into another stream of events.
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
type StreamTransformer = (
|
|
491
|
+
source: AsyncIterable<StreamEvent>,
|
|
492
|
+
) => AsyncIterable<StreamEvent>
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### ResilientStreamOptions
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
interface ResilientStreamOptions {
|
|
499
|
+
checkpointIntervalMs?: number // default: 1000
|
|
500
|
+
maxRetries?: number
|
|
501
|
+
onCheckpoint?: (checkpoint: StreamCheckpoint) => void
|
|
502
|
+
onPartialRecovery?: (text: string, error: Error) => void
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### ElsiumStream
|
|
507
|
+
|
|
508
|
+
An `AsyncIterable<StreamEvent>` wrapper with convenience methods for consuming and transforming LLM streams. Supports only a single consumer — iterating twice throws.
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
class ElsiumStream implements AsyncIterable<StreamEvent> {
|
|
512
|
+
constructor(source: AsyncIterable<StreamEvent>)
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
#### `stream.text()`
|
|
517
|
+
|
|
518
|
+
Returns an `AsyncIterable<string>` that yields only the text deltas.
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
text(): AsyncIterable<string>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
#### `stream.toText()`
|
|
525
|
+
|
|
526
|
+
Collects all text deltas and returns the full text.
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
async toText(): Promise<string>
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### `stream.toTextWithTimeout(timeoutMs)`
|
|
533
|
+
|
|
534
|
+
Like `toText()` but stops collecting after `timeoutMs` milliseconds. Returns whatever text was collected before the deadline.
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
async toTextWithTimeout(timeoutMs: number): Promise<string>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
#### `stream.toResponse()`
|
|
541
|
+
|
|
542
|
+
Collects the full text, token usage, and stop reason from the stream.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
async toResponse(): Promise<{
|
|
546
|
+
text: string
|
|
547
|
+
usage: TokenUsage | null
|
|
548
|
+
stopReason: StopReason | null
|
|
549
|
+
}>
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
#### `stream.pipe(transform)`
|
|
553
|
+
|
|
554
|
+
Creates a new `ElsiumStream` by piping events through a `StreamTransformer`.
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
pipe(transform: StreamTransformer): ElsiumStream
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
#### `stream.resilient(options?)`
|
|
561
|
+
|
|
562
|
+
Wraps the stream with checkpoint and partial-recovery support. Periodically emits `checkpoint` events and, on error, emits a `recovery` event containing whatever text was received before the failure.
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
resilient(options?: ResilientStreamOptions): ElsiumStream
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### createStream()
|
|
569
|
+
|
|
570
|
+
Creates an `ElsiumStream` from an imperative callback. The `emit` function pushes events into a buffered async iterable (max 10,000 events).
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
function createStream(
|
|
574
|
+
executor: (emit: (event: StreamEvent) => void) => Promise<void>,
|
|
575
|
+
): ElsiumStream
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
import { ElsiumStream, createStream } from '@elsium-ai/core'
|
|
580
|
+
|
|
581
|
+
// Create a stream from an imperative source
|
|
582
|
+
const stream = createStream(async (emit) => {
|
|
583
|
+
emit({ type: 'message_start', id: 'msg_1', model: 'claude-sonnet-4-6' })
|
|
584
|
+
emit({ type: 'text_delta', text: 'Hello ' })
|
|
585
|
+
emit({ type: 'text_delta', text: 'world!' })
|
|
586
|
+
emit({ type: 'message_end', usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, stopReason: 'end_turn' })
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
// Consume as full text
|
|
590
|
+
const text = await stream.toText() // "Hello world!"
|
|
591
|
+
|
|
592
|
+
// Or iterate text deltas
|
|
593
|
+
for await (const chunk of stream.text()) {
|
|
594
|
+
process.stdout.write(chunk)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Add resilience with checkpoints
|
|
598
|
+
const resilient = stream.resilient({
|
|
599
|
+
checkpointIntervalMs: 500,
|
|
600
|
+
onCheckpoint: (cp) => console.log('checkpoint:', cp.text.length, 'chars'),
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
// Pipe through a transform
|
|
604
|
+
const filtered = stream.pipe(async function* (source) {
|
|
605
|
+
for await (const event of source) {
|
|
606
|
+
if (event.type !== 'checkpoint') yield event
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Logger
|
|
614
|
+
|
|
615
|
+
### LogLevel
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### LogEntry
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
interface LogEntry {
|
|
625
|
+
level: LogLevel
|
|
626
|
+
message: string
|
|
627
|
+
timestamp: string
|
|
628
|
+
traceId?: string
|
|
629
|
+
data?: Record<string, unknown>
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### LoggerOptions
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
interface LoggerOptions {
|
|
637
|
+
level?: LogLevel // default: 'info'
|
|
638
|
+
pretty?: boolean // default: false (JSON single-line)
|
|
639
|
+
context?: Record<string, unknown> // merged into every entry
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Logger
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
interface Logger {
|
|
647
|
+
debug(message: string, data?: Record<string, unknown>): void
|
|
648
|
+
info(message: string, data?: Record<string, unknown>): void
|
|
649
|
+
warn(message: string, data?: Record<string, unknown>): void
|
|
650
|
+
error(message: string, data?: Record<string, unknown>): void
|
|
651
|
+
child(context: Record<string, unknown>): Logger
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### createLogger()
|
|
656
|
+
|
|
657
|
+
Creates a structured JSON logger. Messages below the configured level are silently dropped. `error` and `warn` go to `console.error`/`console.warn`; everything else goes to `console.log`.
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
function createLogger(options?: LoggerOptions): Logger
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
import { createLogger } from '@elsium-ai/core'
|
|
665
|
+
|
|
666
|
+
const logger = createLogger({ level: 'debug', pretty: true })
|
|
667
|
+
logger.info('server started', { port: 3000 })
|
|
668
|
+
// {"level":"info","message":"server started","timestamp":"...","data":{"port":3000}}
|
|
669
|
+
|
|
670
|
+
const child = logger.child({ traceId: 'trc_abc123' })
|
|
671
|
+
child.warn('slow response', { latencyMs: 4200 })
|
|
672
|
+
// includes traceId in every entry
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Config
|
|
678
|
+
|
|
679
|
+
Type-safe environment variable access. All three functions throw an `ElsiumError` with code `CONFIG_ERROR` when the variable is missing and no fallback is provided.
|
|
680
|
+
|
|
681
|
+
### env()
|
|
682
|
+
|
|
683
|
+
Returns a string environment variable, or the fallback, or throws.
|
|
684
|
+
|
|
685
|
+
```ts
|
|
686
|
+
function env(name: string, fallback?: string): string
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### envNumber()
|
|
690
|
+
|
|
691
|
+
Parses the variable as a finite number. Throws if the value is not a valid finite number.
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
function envNumber(name: string, fallback?: number): number
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### envBool()
|
|
698
|
+
|
|
699
|
+
Parses the variable as a boolean. `'true'`, `'1'`, and `'yes'` (case-insensitive) are truthy; everything else is falsy.
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
function envBool(name: string, fallback?: boolean): boolean
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
```ts
|
|
706
|
+
import { env, envNumber, envBool } from '@elsium-ai/core'
|
|
707
|
+
|
|
708
|
+
const apiKey = env('ANTHROPIC_API_KEY') // throws if missing
|
|
709
|
+
const port = envNumber('PORT', 3000) // 3000 if unset
|
|
710
|
+
const debug = envBool('DEBUG', false) // false if unset
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Utilities
|
|
716
|
+
|
|
717
|
+
### generateId()
|
|
718
|
+
|
|
719
|
+
Generates a unique ID with an optional prefix, using timestamp + 4 random bytes.
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
function generateId(prefix?: string): string // default prefix: 'els'
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Returns a string like `els_m1abc2d_8f3e1a2b`.
|
|
726
|
+
|
|
727
|
+
### generateTraceId()
|
|
728
|
+
|
|
729
|
+
Generates a trace ID using timestamp + 6 random bytes. Always prefixed with `trc_`.
|
|
730
|
+
|
|
731
|
+
```ts
|
|
732
|
+
function generateTraceId(): string
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Returns a string like `trc_m1abc2d_8f3e1a2b4c5d`.
|
|
736
|
+
|
|
737
|
+
### extractText()
|
|
738
|
+
|
|
739
|
+
Extracts plain text from a `Message.content` field, handling both the `string` and `ContentPart[]` forms.
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
function extractText(content: string | { type: string; text?: string }[]): string
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### sleep()
|
|
746
|
+
|
|
747
|
+
Returns a promise that resolves after `ms` milliseconds.
|
|
748
|
+
|
|
749
|
+
```ts
|
|
750
|
+
function sleep(ms: number): Promise<void>
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
### retry()
|
|
754
|
+
|
|
755
|
+
Retries an async function with exponential backoff and jitter. Respects `retryAfterMs` on errors (e.g., `ElsiumError` rate limits).
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
function retry<T>(
|
|
759
|
+
fn: () => Promise<T>,
|
|
760
|
+
options?: {
|
|
761
|
+
maxRetries?: number // default: 3
|
|
762
|
+
baseDelayMs?: number // default: 1000
|
|
763
|
+
maxDelayMs?: number // default: 30000
|
|
764
|
+
shouldRetry?: (error: unknown) => boolean // default: checks error.retryable
|
|
765
|
+
},
|
|
766
|
+
): Promise<T>
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
| Parameter | Type | Default | Description |
|
|
770
|
+
|---|---|---|---|
|
|
771
|
+
| `fn` | `() => Promise<T>` | — | The async operation to retry |
|
|
772
|
+
| `options.maxRetries` | `number` | `3` | Maximum number of retry attempts |
|
|
773
|
+
| `options.baseDelayMs` | `number` | `1000` | Base delay for exponential backoff |
|
|
774
|
+
| `options.maxDelayMs` | `number` | `30000` | Maximum delay cap |
|
|
775
|
+
| `options.shouldRetry` | `(error: unknown) => boolean` | checks `error.retryable` | Predicate to decide whether to retry |
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
import { retry, generateId, generateTraceId, extractText, sleep } from '@elsium-ai/core'
|
|
779
|
+
|
|
780
|
+
const id = generateId() // "els_m1abc2d_8f3e1a2b"
|
|
781
|
+
const traceId = generateTraceId() // "trc_m1abc2d_8f3e1a2b4c5d"
|
|
782
|
+
|
|
783
|
+
// Extract text from either content format
|
|
784
|
+
extractText('hello') // "hello"
|
|
785
|
+
extractText([{ type: 'text', text: 'hello' }]) // "hello"
|
|
786
|
+
|
|
787
|
+
// Retry with defaults (3 retries, exponential backoff)
|
|
788
|
+
const data = await retry(() => fetchFromProvider(), {
|
|
789
|
+
maxRetries: 5,
|
|
790
|
+
shouldRetry: (err) => err instanceof Error,
|
|
791
|
+
})
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## Circuit Breaker
|
|
797
|
+
|
|
798
|
+
Monitors failures within a sliding time window and stops sending traffic to a failing service. Automatically recovers via the half-open state.
|
|
799
|
+
|
|
800
|
+
### CircuitState
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
type CircuitState = 'closed' | 'open' | 'half-open'
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
State machine: **closed** (healthy) → **open** (tripping after threshold failures) → **half-open** (probing after reset timeout) → **closed** (on success) or back to **open** (on failure).
|
|
807
|
+
|
|
808
|
+
### CircuitBreakerConfig
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
interface CircuitBreakerConfig {
|
|
812
|
+
failureThreshold?: number // default: 5
|
|
813
|
+
resetTimeoutMs?: number // default: 30000
|
|
814
|
+
halfOpenMaxAttempts?: number // default: 3
|
|
815
|
+
windowMs?: number // default: 60000
|
|
816
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void
|
|
817
|
+
shouldCount?: (error: unknown) => boolean // default: checks error.retryable, or true for unknown errors
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
| Parameter | Type | Default | Description |
|
|
822
|
+
|---|---|---|---|
|
|
823
|
+
| `failureThreshold` | `number` | `5` | Failures within `windowMs` before opening |
|
|
824
|
+
| `resetTimeoutMs` | `number` | `30000` | Time in open state before probing (half-open) |
|
|
825
|
+
| `halfOpenMaxAttempts` | `number` | `3` | Max concurrent probes in half-open state |
|
|
826
|
+
| `windowMs` | `number` | `60000` | Sliding window for counting failures |
|
|
827
|
+
| `onStateChange` | `(from, to) => void` | — | Callback on state transitions |
|
|
828
|
+
| `shouldCount` | `(error) => boolean` | checks `retryable` | Predicate to decide if an error counts as a failure |
|
|
829
|
+
|
|
830
|
+
### CircuitBreaker
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
interface CircuitBreaker {
|
|
834
|
+
execute<T>(fn: () => Promise<T>): Promise<T>
|
|
835
|
+
readonly state: CircuitState
|
|
836
|
+
readonly failureCount: number
|
|
837
|
+
reset(): void
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
| Member | Description |
|
|
842
|
+
|---|---|
|
|
843
|
+
| `execute(fn)` | Runs `fn` if the circuit is closed or half-open. Throws `ElsiumError` with code `PROVIDER_ERROR` if open. |
|
|
844
|
+
| `state` | Current state. Accessing this may trigger an open → half-open transition if `resetTimeoutMs` has elapsed. |
|
|
845
|
+
| `failureCount` | Number of failures within the current window. |
|
|
846
|
+
| `reset()` | Manually resets to the closed state and clears failure counts. |
|
|
847
|
+
|
|
848
|
+
### createCircuitBreaker()
|
|
849
|
+
|
|
850
|
+
```ts
|
|
851
|
+
function createCircuitBreaker(config?: CircuitBreakerConfig): CircuitBreaker
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
```ts
|
|
855
|
+
import { createCircuitBreaker } from '@elsium-ai/core'
|
|
856
|
+
|
|
857
|
+
const breaker = createCircuitBreaker({
|
|
858
|
+
failureThreshold: 3,
|
|
859
|
+
resetTimeoutMs: 10_000,
|
|
860
|
+
onStateChange: (from, to) => console.log(`circuit: ${from} → ${to}`),
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
const result = await breaker.execute(() => callProvider())
|
|
864
|
+
console.log(breaker.state) // 'closed'
|
|
865
|
+
console.log(breaker.failureCount) // 0
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## Request Dedup
|
|
871
|
+
|
|
872
|
+
Coalesces identical in-flight requests into a single execution and caches results for a short TTL.
|
|
873
|
+
|
|
874
|
+
### DedupConfig
|
|
875
|
+
|
|
876
|
+
```ts
|
|
877
|
+
interface DedupConfig {
|
|
878
|
+
ttlMs?: number // default: 5000
|
|
879
|
+
maxEntries?: number // default: 1000
|
|
880
|
+
}
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### Dedup
|
|
884
|
+
|
|
885
|
+
```ts
|
|
886
|
+
interface Dedup<T> {
|
|
887
|
+
deduplicate(key: string, fn: () => Promise<T>): Promise<T>
|
|
888
|
+
hashRequest(request: unknown): string
|
|
889
|
+
readonly size: number
|
|
890
|
+
clear(): void
|
|
891
|
+
}
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
| Member | Description |
|
|
895
|
+
|---|---|
|
|
896
|
+
| `deduplicate(key, fn)` | Returns a cached result if within TTL, joins an in-flight request if one exists for `key`, or executes `fn`. |
|
|
897
|
+
| `hashRequest(request)` | Deterministic SHA-256 hash (first 16 hex chars) of a JSON-serializable object. Handles key ordering. |
|
|
898
|
+
| `size` | Number of cached + in-flight entries (expired entries are evicted on access). |
|
|
899
|
+
| `clear()` | Clears all cached and in-flight entries. |
|
|
900
|
+
|
|
901
|
+
### createDedup()
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
function createDedup<T>(config?: DedupConfig): Dedup<T>
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
### dedupMiddleware()
|
|
908
|
+
|
|
909
|
+
Returns a `Middleware` that deduplicates LLM requests based on their messages, model, provider, and key completion parameters.
|
|
910
|
+
|
|
911
|
+
```ts
|
|
912
|
+
function dedupMiddleware(config?: DedupConfig): Middleware
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
```ts
|
|
916
|
+
import { createDedup, dedupMiddleware } from '@elsium-ai/core'
|
|
917
|
+
|
|
918
|
+
// Standalone usage
|
|
919
|
+
const dedup = createDedup<string>({ ttlMs: 10_000 })
|
|
920
|
+
const key = dedup.hashRequest({ model: 'claude-sonnet-4-6', messages: [...] })
|
|
921
|
+
const result = await dedup.deduplicate(key, () => expensive())
|
|
922
|
+
|
|
923
|
+
// As middleware — identical concurrent requests share one API call
|
|
924
|
+
const middleware = dedupMiddleware({ ttlMs: 3000 })
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## Policy Engine
|
|
930
|
+
|
|
931
|
+
Declarative rules to allow or deny LLM requests before they reach a provider.
|
|
932
|
+
|
|
933
|
+
### PolicyDecision
|
|
934
|
+
|
|
935
|
+
```ts
|
|
936
|
+
type PolicyDecision = 'allow' | 'deny'
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### PolicyResult
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
interface PolicyResult {
|
|
943
|
+
decision: PolicyDecision
|
|
944
|
+
reason: string
|
|
945
|
+
policyName: string
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### PolicyContext
|
|
950
|
+
|
|
951
|
+
The evaluation context passed to every policy rule.
|
|
952
|
+
|
|
953
|
+
```ts
|
|
954
|
+
interface PolicyContext {
|
|
955
|
+
model?: string
|
|
956
|
+
provider?: string
|
|
957
|
+
actor?: string
|
|
958
|
+
role?: string
|
|
959
|
+
tokenCount?: number
|
|
960
|
+
costEstimate?: number
|
|
961
|
+
requestContent?: string
|
|
962
|
+
metadata?: Record<string, unknown>
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### PolicyRule
|
|
967
|
+
|
|
968
|
+
```ts
|
|
969
|
+
type PolicyRule = (ctx: PolicyContext) => PolicyResult
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### PolicyConfig
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
interface PolicyConfig {
|
|
976
|
+
name: string
|
|
977
|
+
description?: string
|
|
978
|
+
rules: PolicyRule[]
|
|
979
|
+
mode?: 'all-must-pass' | 'any-must-pass' // default: 'all-must-pass'
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### PolicySet
|
|
984
|
+
|
|
985
|
+
```ts
|
|
986
|
+
interface PolicySet {
|
|
987
|
+
evaluate(ctx: PolicyContext): PolicyResult[]
|
|
988
|
+
addPolicy(policy: PolicyConfig): void
|
|
989
|
+
removePolicy(name: string): void
|
|
990
|
+
readonly policies: string[]
|
|
991
|
+
}
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
| Member | Description |
|
|
995
|
+
|---|---|
|
|
996
|
+
| `evaluate(ctx)` | Runs all policy rules and returns an array of **denials only** (empty = all passed). |
|
|
997
|
+
| `addPolicy(policy)` | Adds a policy at runtime. |
|
|
998
|
+
| `removePolicy(name)` | Removes a policy by name. |
|
|
999
|
+
| `policies` | List of currently registered policy names. |
|
|
1000
|
+
|
|
1001
|
+
### createPolicySet()
|
|
1002
|
+
|
|
1003
|
+
```ts
|
|
1004
|
+
function createPolicySet(policies: PolicyConfig[]): PolicySet
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
### policyMiddleware()
|
|
1008
|
+
|
|
1009
|
+
Returns a `Middleware` that evaluates all policies before forwarding the request. Throws `ElsiumError` with code `VALIDATION_ERROR` if any policy denies.
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
function policyMiddleware(policySet: PolicySet): Middleware
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Built-in policy factories
|
|
1016
|
+
|
|
1017
|
+
#### modelAccessPolicy()
|
|
1018
|
+
|
|
1019
|
+
Restricts requests to a list of allowed models. Supports glob-style trailing wildcards (e.g., `'claude-*'`).
|
|
1020
|
+
|
|
1021
|
+
```ts
|
|
1022
|
+
function modelAccessPolicy(allowedModels: string[]): PolicyConfig
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
#### tokenLimitPolicy()
|
|
1026
|
+
|
|
1027
|
+
Denies requests whose estimated token count exceeds `maxTokens`.
|
|
1028
|
+
|
|
1029
|
+
```ts
|
|
1030
|
+
function tokenLimitPolicy(maxTokens: number): PolicyConfig
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
#### costLimitPolicy()
|
|
1034
|
+
|
|
1035
|
+
Denies requests whose estimated cost exceeds `maxCost`.
|
|
1036
|
+
|
|
1037
|
+
```ts
|
|
1038
|
+
function costLimitPolicy(maxCost: number): PolicyConfig
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
#### contentPolicy()
|
|
1042
|
+
|
|
1043
|
+
Denies requests whose content matches any of the provided regex patterns.
|
|
1044
|
+
|
|
1045
|
+
```ts
|
|
1046
|
+
function contentPolicy(blockedPatterns: RegExp[]): PolicyConfig
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
```ts
|
|
28
1050
|
import {
|
|
29
|
-
createCircuitBreaker,
|
|
30
1051
|
createPolicySet,
|
|
1052
|
+
policyMiddleware,
|
|
31
1053
|
modelAccessPolicy,
|
|
1054
|
+
tokenLimitPolicy,
|
|
32
1055
|
costLimitPolicy,
|
|
33
|
-
|
|
34
|
-
env,
|
|
1056
|
+
contentPolicy,
|
|
35
1057
|
} from '@elsium-ai/core'
|
|
36
1058
|
|
|
37
|
-
// Circuit breaker
|
|
38
|
-
const cb = createCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 })
|
|
39
|
-
const result = await cb.execute(() => fetchFromProvider())
|
|
40
|
-
|
|
41
|
-
// Policy engine
|
|
42
1059
|
const policies = createPolicySet([
|
|
43
1060
|
modelAccessPolicy(['claude-sonnet-4-6', 'gpt-4o']),
|
|
1061
|
+
tokenLimitPolicy(100_000),
|
|
44
1062
|
costLimitPolicy(5.00),
|
|
1063
|
+
contentPolicy([/password/i, /secret_key/i]),
|
|
45
1064
|
])
|
|
1065
|
+
|
|
1066
|
+
// Check manually
|
|
1067
|
+
const denials = policies.evaluate({ model: 'unknown-model' })
|
|
1068
|
+
// [{ decision: 'deny', reason: 'Model "unknown-model" is not in allowed list', policyName: 'model-access' }]
|
|
1069
|
+
|
|
1070
|
+
// Or use as middleware
|
|
1071
|
+
const middleware = policyMiddleware(policies)
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
---
|
|
1075
|
+
|
|
1076
|
+
## Shutdown Manager
|
|
1077
|
+
|
|
1078
|
+
Tracks in-flight operations and drains them before process exit. Automatically registers signal handlers for `SIGTERM` and `SIGINT`.
|
|
1079
|
+
|
|
1080
|
+
### ShutdownConfig
|
|
1081
|
+
|
|
1082
|
+
```ts
|
|
1083
|
+
interface ShutdownConfig {
|
|
1084
|
+
drainTimeoutMs?: number // default: 30000
|
|
1085
|
+
signals?: string[] // default: ['SIGTERM', 'SIGINT']
|
|
1086
|
+
onDrainStart?: () => void
|
|
1087
|
+
onDrainComplete?: () => void
|
|
1088
|
+
onForceShutdown?: () => void
|
|
1089
|
+
}
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
| Parameter | Type | Default | Description |
|
|
1093
|
+
|---|---|---|---|
|
|
1094
|
+
| `drainTimeoutMs` | `number` | `30000` | Max time to wait for in-flight operations to finish |
|
|
1095
|
+
| `signals` | `string[]` | `['SIGTERM', 'SIGINT']` | OS signals that trigger shutdown |
|
|
1096
|
+
| `onDrainStart` | `() => void` | — | Called when drain begins |
|
|
1097
|
+
| `onDrainComplete` | `() => void` | — | Called when all operations finish within timeout |
|
|
1098
|
+
| `onForceShutdown` | `() => void` | — | Called when drain timeout expires |
|
|
1099
|
+
|
|
1100
|
+
### ShutdownManager
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
interface ShutdownManager {
|
|
1104
|
+
trackOperation<T>(fn: () => Promise<T>): Promise<T>
|
|
1105
|
+
shutdown(): Promise<void>
|
|
1106
|
+
dispose(): void
|
|
1107
|
+
readonly inFlight: number
|
|
1108
|
+
readonly isShuttingDown: boolean
|
|
1109
|
+
}
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
| Member | Description |
|
|
1113
|
+
|---|---|
|
|
1114
|
+
| `trackOperation(fn)` | Executes `fn` while tracking it as in-flight. Throws `ElsiumError` with code `VALIDATION_ERROR` if already shutting down. |
|
|
1115
|
+
| `shutdown()` | Initiates graceful shutdown. Waits for in-flight operations up to `drainTimeoutMs`. Idempotent — multiple calls return the same promise. |
|
|
1116
|
+
| `dispose()` | Removes all registered signal handlers. Call this in tests to prevent leaks. |
|
|
1117
|
+
| `inFlight` | Number of currently tracked operations. |
|
|
1118
|
+
| `isShuttingDown` | `true` after `shutdown()` is called. |
|
|
1119
|
+
|
|
1120
|
+
### createShutdownManager()
|
|
1121
|
+
|
|
1122
|
+
```ts
|
|
1123
|
+
function createShutdownManager(config?: ShutdownConfig): ShutdownManager
|
|
46
1124
|
```
|
|
47
1125
|
|
|
1126
|
+
```ts
|
|
1127
|
+
import { createShutdownManager } from '@elsium-ai/core'
|
|
1128
|
+
|
|
1129
|
+
const shutdown = createShutdownManager({
|
|
1130
|
+
drainTimeoutMs: 10_000,
|
|
1131
|
+
onDrainStart: () => console.log('draining...'),
|
|
1132
|
+
onDrainComplete: () => console.log('drained, exiting'),
|
|
1133
|
+
onForceShutdown: () => console.log('force shutdown!'),
|
|
1134
|
+
})
|
|
1135
|
+
|
|
1136
|
+
// Wrap every request
|
|
1137
|
+
const result = await shutdown.trackOperation(() => handleRequest())
|
|
1138
|
+
console.log(shutdown.inFlight) // 0 after completion
|
|
1139
|
+
|
|
1140
|
+
// Cleanup in tests
|
|
1141
|
+
shutdown.dispose()
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
---
|
|
1145
|
+
|
|
48
1146
|
## Part of ElsiumAI
|
|
49
1147
|
|
|
50
1148
|
This package is the foundation layer of the [ElsiumAI](https://github.com/elsium-ai/elsium-ai) framework. See the [full documentation](https://github.com/elsium-ai/elsium-ai) for guides and examples.
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAU3D;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAqBjE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAU3D;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAqBjE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAoBjE"}
|
package/dist/index.js
CHANGED
|
@@ -476,7 +476,16 @@ function envBool(name, fallback) {
|
|
|
476
476
|
const raw = getEnvVar(name);
|
|
477
477
|
if (raw !== undefined) {
|
|
478
478
|
const normalized = raw.toLowerCase();
|
|
479
|
-
|
|
479
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes")
|
|
480
|
+
return true;
|
|
481
|
+
if (normalized === "false" || normalized === "0" || normalized === "no")
|
|
482
|
+
return false;
|
|
483
|
+
throw new ElsiumError({
|
|
484
|
+
code: "CONFIG_ERROR",
|
|
485
|
+
message: `Environment variable ${name} has unrecognized boolean value: ${raw}. Expected one of: true, false, 1, 0, yes, no`,
|
|
486
|
+
retryable: false,
|
|
487
|
+
metadata: { variable: name, value: raw }
|
|
488
|
+
});
|
|
480
489
|
}
|
|
481
490
|
if (fallback !== undefined)
|
|
482
491
|
return fallback;
|
package/dist/stream.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { StopReason, StreamCheckpoint, StreamEvent, TokenUsage } from './types';
|
|
2
2
|
export interface ResilientStreamOptions {
|
|
3
3
|
checkpointIntervalMs?: number;
|
|
4
|
-
maxRetries?: number;
|
|
5
4
|
onCheckpoint?: (checkpoint: StreamCheckpoint) => void;
|
|
6
5
|
onPartialRecovery?: (text: string, error: Error) => void;
|
|
7
6
|
}
|
package/dist/stream.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGpF,MAAM,WAAW,sBAAsB;IACtC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGpF,MAAM,WAAW,sBAAsB;IACtC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACrD,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACxD;AA2CD,qBAAa,YAAa,YAAW,aAAa,CAAC,WAAW,CAAC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,SAAS,CAAQ;gBAEb,MAAM,EAAE,aAAa,CAAC,WAAW,CAAC;IAIvC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,qBAAqB,CAAC,WAAW,CAAC;IAQnE,IAAI,IAAI,aAAa,CAAC,MAAM,CAAC;IAavB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAQzB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkCrD,UAAU,IAAI,OAAO,CAAC;QAC3B,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;QACxB,UAAU,EAAE,UAAU,GAAG,IAAI,CAAA;KAC7B,CAAC;IAoBF,IAAI,CAAC,SAAS,EAAE,iBAAiB,GAAG,YAAY;IAIhD,SAAS,CAAC,OAAO,GAAE,sBAA2B,GAAG,YAAY;CAsC7D;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,WAAW,CAAC,KAAK,aAAa,CAAC,WAAW,CAAC,CAAA;AAIlG,wBAAgB,YAAY,CAC3B,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,GAC7D,YAAY,CAmEd"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elsium-ai/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Core types, schemas, errors, and utilities for ElsiumAI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Utrera <ebutrera9103@gmail.com>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/elsium-ai/elsium-ai",
|
|
9
|
+
"url": "git+https://github.com/elsium-ai/elsium-ai.git",
|
|
10
10
|
"directory": "packages/core"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|