@elsium-ai/core 0.2.0 → 0.2.2

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.
Files changed (2) hide show
  1. package/README.md +1117 -19
  2. package/package.json +6 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @elsium-ai/core
2
2
 
3
- Core types, schemas, errors, and utilities for [ElsiumAI](https://github.com/elsium-ai/elsium-ai).
3
+ Core types, errors, result pattern, streaming, and infrastructure utilities for [ElsiumAI](https://github.com/elsium-ai/elsium-ai).
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@elsium-ai/core.svg)](https://www.npmjs.com/package/@elsium-ai/core)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
- - **Types** `CompletionRequest`, `LLMResponse`, `Message`, `Middleware`, and all shared interfaces
17
- - **Circuit Breaker** — Detects failing providers, stops sending traffic, auto-recovers
18
- - **Request Dedup** Identical in-flight calls coalesce into one API request
19
- - **Policy Engine** Declarative rules to deny by model, cost, token count, or content pattern
20
- - **Graceful Shutdown** Drains in-flight operations before process exit
21
- - **Retry with Backoff** Exponential backoff with jitter
22
- - **Logger** Structured logging with levels and context
23
- - **Config** Type-safe environment variable access via `env()`
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
- ## Usage
30
+ ---
26
31
 
27
- ```typescript
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
- policyMiddleware,
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/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@elsium-ai/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
@@ -30,5 +30,9 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "typescript": "^5.7.0"
33
+ },
34
+ "publishConfig": {
35
+ "registry": "https://registry.npmjs.org",
36
+ "access": "public"
33
37
  }
34
38
  }