@relayplane/proxy 1.5.46 → 1.7.1
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 +251 -15
- package/assets/relayplane-proxy.service +20 -0
- package/dist/alerts.d.ts +72 -0
- package/dist/alerts.d.ts.map +1 -0
- package/dist/alerts.js +290 -0
- package/dist/alerts.js.map +1 -0
- package/dist/anomaly.d.ts +65 -0
- package/dist/anomaly.d.ts.map +1 -0
- package/dist/anomaly.js +193 -0
- package/dist/anomaly.js.map +1 -0
- package/dist/budget.d.ts +98 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +356 -0
- package/dist/budget.js.map +1 -0
- package/dist/cli.js +512 -93
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +28 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +122 -24
- package/dist/config.js.map +1 -1
- package/dist/downgrade.d.ts +37 -0
- package/dist/downgrade.d.ts.map +1 -0
- package/dist/downgrade.js +79 -0
- package/dist/downgrade.js.map +1 -0
- package/dist/mesh/capture.d.ts +11 -0
- package/dist/mesh/capture.d.ts.map +1 -0
- package/dist/mesh/capture.js +43 -0
- package/dist/mesh/capture.js.map +1 -0
- package/dist/mesh/fitness.d.ts +14 -0
- package/dist/mesh/fitness.d.ts.map +1 -0
- package/dist/mesh/fitness.js +40 -0
- package/dist/mesh/fitness.js.map +1 -0
- package/dist/mesh/index.d.ts +39 -0
- package/dist/mesh/index.d.ts.map +1 -0
- package/dist/mesh/index.js +118 -0
- package/dist/mesh/index.js.map +1 -0
- package/dist/mesh/store.d.ts +30 -0
- package/dist/mesh/store.d.ts.map +1 -0
- package/dist/mesh/store.js +174 -0
- package/dist/mesh/store.js.map +1 -0
- package/dist/mesh/sync.d.ts +37 -0
- package/dist/mesh/sync.d.ts.map +1 -0
- package/dist/mesh/sync.js +154 -0
- package/dist/mesh/sync.js.map +1 -0
- package/dist/mesh/types.d.ts +57 -0
- package/dist/mesh/types.d.ts.map +1 -0
- package/dist/mesh/types.js +7 -0
- package/dist/mesh/types.js.map +1 -0
- package/dist/rate-limiter.d.ts +64 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +159 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/relay-config.d.ts +9 -0
- package/dist/relay-config.d.ts.map +1 -1
- package/dist/relay-config.js +2 -0
- package/dist/relay-config.js.map +1 -1
- package/dist/response-cache.d.ts +139 -0
- package/dist/response-cache.d.ts.map +1 -0
- package/dist/response-cache.js +515 -0
- package/dist/response-cache.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -1
- package/dist/server.js.map +1 -1
- package/dist/standalone-proxy.d.ts +2 -1
- package/dist/standalone-proxy.d.ts.map +1 -1
- package/dist/standalone-proxy.js +662 -26
- package/dist/standalone-proxy.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +8 -5
- package/dist/telemetry.js.map +1 -1
- package/dist/utils/model-suggestions.d.ts.map +1 -1
- package/dist/utils/model-suggestions.js +19 -2
- package/dist/utils/model-suggestions.js.map +1 -1
- package/dist/utils/version-status.d.ts +9 -0
- package/dist/utils/version-status.d.ts.map +1 -0
- package/dist/utils/version-status.js +28 -0
- package/dist/utils/version-status.js.map +1 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -11,6 +11,15 @@ An open-source LLM proxy that sits between your AI agents and providers. Tracks
|
|
|
11
11
|
- 🔀 Configurable task-aware routing (complexity-based, cascade, model overrides)
|
|
12
12
|
- 🛡️ Circuit breaker architecture — if the proxy fails, your agent doesn't notice
|
|
13
13
|
- 📈 Local dashboard with cost breakdown, savings analysis, and provider health
|
|
14
|
+
- 💵 **Budget enforcement** — daily/hourly/per-request spend limits with block, warn, downgrade, or alert actions
|
|
15
|
+
- 🔍 **Anomaly detection** — catches runaway agent loops, cost spikes, and token explosions in real time
|
|
16
|
+
- 🔔 **Cost alerts** — threshold alerts at configurable percentages, webhook delivery, alert history
|
|
17
|
+
- ⬇️ **Auto-downgrade** — automatically switches to cheaper models when budget thresholds are hit
|
|
18
|
+
- 📦 **Aggressive cache** — exact-match and aggressive response caching with gzipped disk persistence
|
|
19
|
+
- 🧠 **Osmosis mesh** — opt-in collective learning layer that shares anonymized routing signals across users
|
|
20
|
+
- 🔧 **systemd/launchd service** — `relayplane service install` for always-on operation with auto-restart
|
|
21
|
+
- 🏥 **Health watchdog** — `/health` endpoint with uptime tracking and active probing
|
|
22
|
+
- 🛡️ **Config resilience** — atomic writes, automatic backup/restore, credential separation
|
|
14
23
|
|
|
15
24
|
## Quick Start
|
|
16
25
|
|
|
@@ -55,29 +64,38 @@ A minimal config file:
|
|
|
55
64
|
|
|
56
65
|
All configuration is optional — sensible defaults are applied for every field. The proxy merges your config with its defaults via deep merge, so you only need to specify what you want to change.
|
|
57
66
|
|
|
58
|
-
## Architecture
|
|
67
|
+
## Architecture
|
|
59
68
|
|
|
60
69
|
```text
|
|
61
70
|
Client (Claude Code / Aider / Cursor)
|
|
62
71
|
|
|
|
63
72
|
| OpenAI/Anthropic-compatible request
|
|
64
73
|
v
|
|
65
|
-
|
|
66
|
-
| RelayPlane Proxy (local)
|
|
67
|
-
|
|
68
|
-
| 1) Parse request
|
|
69
|
-
| 2)
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
| 5)
|
|
76
|
-
|
|
|
77
|
-
|
|
74
|
+
+-------------------------------------------------------+
|
|
75
|
+
| RelayPlane Proxy (local) |
|
|
76
|
+
|-------------------------------------------------------|
|
|
77
|
+
| 1) Parse request |
|
|
78
|
+
| 2) Cache check (exact or aggressive mode) |
|
|
79
|
+
| └─ HIT → return cached response (skip provider) |
|
|
80
|
+
| 3) Budget check (daily/hourly/per-request limits) |
|
|
81
|
+
| └─ BREACH → block / warn / downgrade / alert |
|
|
82
|
+
| 4) Anomaly detection (velocity, cost spike, loops) |
|
|
83
|
+
| └─ DETECTED → alert + optional block |
|
|
84
|
+
| 5) Auto-downgrade (if budget threshold exceeded) |
|
|
85
|
+
| └─ Rewrite model to cheaper alternative |
|
|
86
|
+
| 6) Infer task/complexity (pre-request) |
|
|
87
|
+
| 7) Select route/model |
|
|
88
|
+
| - explicit model / passthrough |
|
|
89
|
+
| - relayplane:auto/cost/fast/quality |
|
|
90
|
+
| - configured complexity/cascade rules |
|
|
91
|
+
| 8) Forward request to provider |
|
|
92
|
+
| 9) Return provider response + cache it |
|
|
93
|
+
| 10) Record telemetry + update budget tracking |
|
|
94
|
+
| 11) Mesh sync (push anonymized routing signals) |
|
|
95
|
+
+-------------------------------------------------------+
|
|
78
96
|
|
|
|
79
97
|
v
|
|
80
|
-
Provider APIs (Anthropic/OpenAI/Gemini/xAI
|
|
98
|
+
Provider APIs (Anthropic/OpenAI/Gemini/xAI/...)
|
|
81
99
|
```
|
|
82
100
|
|
|
83
101
|
## How It Works
|
|
@@ -315,10 +333,228 @@ The dashboard is powered by JSON endpoints you can use directly:
|
|
|
315
333
|
| `GET /v1/telemetry/savings` | Cost savings from smart routing |
|
|
316
334
|
| `GET /v1/telemetry/health` | Provider health and cooldown status |
|
|
317
335
|
|
|
336
|
+
## Budget Enforcement
|
|
337
|
+
|
|
338
|
+
Set spending limits to prevent runaway costs. The budget manager tracks spend in rolling daily and hourly windows using SQLite with an in-memory cache for <5ms hot-path checks.
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"budget": {
|
|
343
|
+
"enabled": true,
|
|
344
|
+
"dailyUsd": 50,
|
|
345
|
+
"hourlyUsd": 10,
|
|
346
|
+
"perRequestUsd": 2,
|
|
347
|
+
"onBreach": "downgrade",
|
|
348
|
+
"downgradeTo": "claude-sonnet-4-6",
|
|
349
|
+
"alertThresholds": [50, 80, 95]
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
| Field | Default | Description |
|
|
355
|
+
|-------|---------|-------------|
|
|
356
|
+
| `enabled` | `false` | Enable budget enforcement |
|
|
357
|
+
| `dailyUsd` | `50` | Daily spend limit |
|
|
358
|
+
| `hourlyUsd` | `10` | Hourly spend limit |
|
|
359
|
+
| `perRequestUsd` | `2` | Max cost for a single request |
|
|
360
|
+
| `onBreach` | `"downgrade"` | Action: `block`, `warn`, `downgrade`, or `alert` |
|
|
361
|
+
| `downgradeTo` | `"claude-sonnet-4-6"` | Model to use when downgrading |
|
|
362
|
+
| `alertThresholds` | `[50, 80, 95]` | Fire alerts at these % of daily limit |
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
relayplane budget status # See current spend vs limits
|
|
366
|
+
relayplane budget set --daily 25 # Change daily limit
|
|
367
|
+
relayplane budget set --hourly 5 # Change hourly limit
|
|
368
|
+
relayplane budget reset # Reset spend counters
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Anomaly Detection
|
|
372
|
+
|
|
373
|
+
Catches runaway agent loops and cost spikes using a sliding window over the last 100 requests.
|
|
374
|
+
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"anomaly": {
|
|
378
|
+
"enabled": true,
|
|
379
|
+
"velocityThreshold": 50,
|
|
380
|
+
"tokenExplosionUsd": 5.0,
|
|
381
|
+
"repetitionThreshold": 20,
|
|
382
|
+
"windowMs": 300000
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Detection types:**
|
|
388
|
+
|
|
389
|
+
| Type | Triggers when... |
|
|
390
|
+
|------|-------------------|
|
|
391
|
+
| `velocity_spike` | Request rate exceeds threshold in 5-minute window |
|
|
392
|
+
| `cost_acceleration` | Spend rate is doubling every minute |
|
|
393
|
+
| `repetition` | Same model + similar token count >20 times in 5 min |
|
|
394
|
+
| `token_explosion` | Single request estimated cost exceeds $5 |
|
|
395
|
+
|
|
396
|
+
## Cost Alerts
|
|
397
|
+
|
|
398
|
+
Get notified when spending crosses thresholds. Alerts are deduplicated per window and stored in SQLite for history.
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"alerts": {
|
|
403
|
+
"enabled": true,
|
|
404
|
+
"webhookUrl": "https://hooks.slack.com/...",
|
|
405
|
+
"cooldownMs": 300000,
|
|
406
|
+
"maxHistory": 500
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Alert types: `threshold` (budget %), `anomaly` (detection triggers), `breach` (limit exceeded). Severity levels: `info`, `warning`, `critical`.
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
relayplane alerts list # Show recent alerts
|
|
415
|
+
relayplane alerts counts # Count by type (threshold/anomaly/breach)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Auto-Downgrade
|
|
419
|
+
|
|
420
|
+
When budget hits a configurable threshold (default 80%), the proxy automatically rewrites expensive models to cheaper alternatives. Adds `X-RelayPlane-Downgraded` headers so your agent knows.
|
|
421
|
+
|
|
422
|
+
```json
|
|
423
|
+
{
|
|
424
|
+
"downgrade": {
|
|
425
|
+
"enabled": true,
|
|
426
|
+
"thresholdPercent": 80,
|
|
427
|
+
"mapping": {
|
|
428
|
+
"claude-opus-4-6": "claude-sonnet-4-6",
|
|
429
|
+
"gpt-4o": "gpt-4o-mini",
|
|
430
|
+
"gemini-2.5-pro": "gemini-2.0-flash"
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Built-in mappings cover all major Anthropic, OpenAI, and Google models. Override with your own.
|
|
437
|
+
|
|
438
|
+
## Response Cache
|
|
439
|
+
|
|
440
|
+
Caches LLM responses to avoid duplicate API calls. SHA-256 hash of the canonical request → cached response with gzipped disk persistence.
|
|
441
|
+
|
|
442
|
+
```json
|
|
443
|
+
{
|
|
444
|
+
"cache": {
|
|
445
|
+
"enabled": true,
|
|
446
|
+
"mode": "exact",
|
|
447
|
+
"maxSizeMb": 100,
|
|
448
|
+
"defaultTtlSeconds": 3600,
|
|
449
|
+
"onlyWhenDeterministic": true
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
| Mode | Behavior |
|
|
455
|
+
|------|----------|
|
|
456
|
+
| `exact` | Cache only identical requests (default) |
|
|
457
|
+
| `aggressive` | Broader matching with shorter TTL (30 min default) |
|
|
458
|
+
|
|
459
|
+
Only caches deterministic requests (temperature=0) by default. Skips responses with tool calls.
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
relayplane cache status # Entries, size, hit rate, saved cost
|
|
463
|
+
relayplane cache stats # Detailed breakdown by model and task type
|
|
464
|
+
relayplane cache clear # Wipe the cache
|
|
465
|
+
relayplane cache on/off # Toggle caching
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Osmosis Mesh
|
|
469
|
+
|
|
470
|
+
Opt-in collective learning layer. Share anonymized routing signals (model, task type, tokens, cost — never prompts) and benefit from the network's routing intelligence.
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"mesh": {
|
|
475
|
+
"enabled": true,
|
|
476
|
+
"endpoint": "https://osmosis-mesh-dev.fly.dev",
|
|
477
|
+
"sync_interval_ms": 60000,
|
|
478
|
+
"contribute": true
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Auto-enabled for authenticated users. Contribution is opt-in — set `contribute: false` to consume signals without sharing.
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
relayplane mesh status # Atoms local/synced, last sync, endpoint
|
|
487
|
+
relayplane mesh on/off # Enable/disable mesh
|
|
488
|
+
relayplane mesh sync # Force sync now
|
|
489
|
+
relayplane mesh contribute on/off # Toggle contribution
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## System Service
|
|
493
|
+
|
|
494
|
+
Install RelayPlane as a system service for always-on operation with auto-restart on crash.
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
# Linux (systemd)
|
|
498
|
+
sudo relayplane service install # Install + enable + start
|
|
499
|
+
sudo relayplane service uninstall # Stop + disable + remove
|
|
500
|
+
relayplane service status # Check service state
|
|
501
|
+
|
|
502
|
+
# macOS (launchd)
|
|
503
|
+
relayplane service install # Install as LaunchAgent
|
|
504
|
+
relayplane service uninstall # Remove LaunchAgent
|
|
505
|
+
relayplane service status # Check loaded state
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
The service unit includes `WatchdogSec=30` (systemd) and `KeepAlive` (launchd) for automatic health monitoring and restart. API keys from your current environment are captured into the service definition.
|
|
509
|
+
|
|
510
|
+
## Config Resilience
|
|
511
|
+
|
|
512
|
+
Configuration is protected against corruption:
|
|
513
|
+
|
|
514
|
+
- **Atomic writes** — config is written to a `.tmp` file then renamed (no partial writes)
|
|
515
|
+
- **Automatic backup** — `config.json.bak` is updated before every save
|
|
516
|
+
- **Auto-restore** — if `config.json` is corrupt/missing, the proxy restores from backup
|
|
517
|
+
- **Credential separation** — API keys live in `credentials.json`, surviving config resets
|
|
518
|
+
|
|
318
519
|
## Circuit Breaker
|
|
319
520
|
|
|
320
521
|
If the proxy ever fails, all traffic automatically bypasses it — your agent talks directly to the provider. When RelayPlane recovers, traffic resumes. No manual intervention needed.
|
|
321
522
|
|
|
523
|
+
## CLI Reference
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
relayplane [command] [options]
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
| Command | Description |
|
|
530
|
+
|---------|-------------|
|
|
531
|
+
| `(default)` / `start` | Start the proxy server |
|
|
532
|
+
| `init` | Initialize config and show setup instructions |
|
|
533
|
+
| `status` | Show proxy status, plan, and cloud sync info |
|
|
534
|
+
| `login` | Log in to RelayPlane (device OAuth flow) |
|
|
535
|
+
| `logout` | Clear stored credentials |
|
|
536
|
+
| `upgrade` | Open pricing page |
|
|
537
|
+
| `enable` / `disable` | Toggle proxy routing in OpenClaw config |
|
|
538
|
+
| `telemetry on\|off\|status` | Manage telemetry |
|
|
539
|
+
| `stats` | Show usage statistics and savings |
|
|
540
|
+
| `config [set-key <key>]` | Show or update configuration |
|
|
541
|
+
| `budget status\|set\|reset` | Manage spend limits |
|
|
542
|
+
| `alerts list\|counts` | View cost alert history |
|
|
543
|
+
| `cache status\|stats\|clear\|on\|off` | Manage response cache |
|
|
544
|
+
| `mesh status\|on\|off\|sync\|contribute` | Manage Osmosis mesh |
|
|
545
|
+
| `service install\|uninstall\|status` | System service management |
|
|
546
|
+
| `autostart on\|off\|status` | Legacy autostart (systemd) |
|
|
547
|
+
|
|
548
|
+
**Server options:**
|
|
549
|
+
|
|
550
|
+
| Flag | Default | Description |
|
|
551
|
+
|------|---------|-------------|
|
|
552
|
+
| `--port <n>` | `4100` | Port to listen on |
|
|
553
|
+
| `--host <s>` | `127.0.0.1` | Host to bind to |
|
|
554
|
+
| `--offline` | — | No network calls except LLM endpoints |
|
|
555
|
+
| `--audit` | — | Show telemetry payloads before sending |
|
|
556
|
+
| `-v, --verbose` | — | Verbose logging |
|
|
557
|
+
|
|
322
558
|
## Your Keys Stay Yours
|
|
323
559
|
|
|
324
560
|
RelayPlane requires your own provider API keys. Your prompts go directly to LLM providers — never through RelayPlane servers. All proxy execution is local. Telemetry (anonymous metadata only) is opt-in.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=RelayPlane Proxy - Intelligent AI Model Routing
|
|
3
|
+
After=network.target
|
|
4
|
+
StartLimitIntervalSec=300
|
|
5
|
+
StartLimitBurst=5
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=notify
|
|
9
|
+
User=root
|
|
10
|
+
ExecStart=/usr/bin/relayplane
|
|
11
|
+
Restart=always
|
|
12
|
+
RestartSec=5
|
|
13
|
+
WatchdogSec=30
|
|
14
|
+
StandardOutput=journal
|
|
15
|
+
StandardError=journal
|
|
16
|
+
Environment=HOME=/root
|
|
17
|
+
Environment=NODE_ENV=production
|
|
18
|
+
|
|
19
|
+
[Install]
|
|
20
|
+
WantedBy=multi-user.target
|
package/dist/alerts.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RelayPlane Cost Alerts & Webhooks
|
|
3
|
+
*
|
|
4
|
+
* Alert types: threshold (budget %), anomaly, breach.
|
|
5
|
+
* Webhook delivery via POST to configured URL.
|
|
6
|
+
* SQLite storage for alert history.
|
|
7
|
+
* Alert deduplication per window.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import type { AnomalyDetail } from './anomaly.js';
|
|
12
|
+
export interface AlertsConfig {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** Webhook URL for alert delivery */
|
|
15
|
+
webhookUrl?: string;
|
|
16
|
+
/** Alert cooldown in ms to prevent spam (default: 300000 = 5 min) */
|
|
17
|
+
cooldownMs: number;
|
|
18
|
+
/** Max alerts stored in history */
|
|
19
|
+
maxHistory: number;
|
|
20
|
+
}
|
|
21
|
+
export type AlertType = 'threshold' | 'anomaly' | 'breach';
|
|
22
|
+
export interface Alert {
|
|
23
|
+
id: string;
|
|
24
|
+
type: AlertType;
|
|
25
|
+
message: string;
|
|
26
|
+
severity: 'info' | 'warning' | 'critical';
|
|
27
|
+
timestamp: number;
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
delivered: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare const DEFAULT_ALERTS_CONFIG: AlertsConfig;
|
|
32
|
+
export declare class AlertManager {
|
|
33
|
+
private config;
|
|
34
|
+
private db;
|
|
35
|
+
private _initialized;
|
|
36
|
+
private dedup;
|
|
37
|
+
private memoryAlerts;
|
|
38
|
+
private alertCounter;
|
|
39
|
+
constructor(config?: Partial<AlertsConfig>);
|
|
40
|
+
/** Initialize SQLite storage */
|
|
41
|
+
init(): void;
|
|
42
|
+
updateConfig(config: Partial<AlertsConfig>): void;
|
|
43
|
+
getConfig(): AlertsConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Fire a threshold alert (budget % crossed)
|
|
46
|
+
*/
|
|
47
|
+
fireThreshold(threshold: number, currentPercent: number, currentSpend: number, limit: number): Alert | null;
|
|
48
|
+
/**
|
|
49
|
+
* Fire an anomaly alert
|
|
50
|
+
*/
|
|
51
|
+
fireAnomaly(anomaly: AnomalyDetail): Alert | null;
|
|
52
|
+
/**
|
|
53
|
+
* Fire a breach alert (budget limit exceeded)
|
|
54
|
+
*/
|
|
55
|
+
fireBreach(breachType: string, currentSpend: number, limit: number): Alert | null;
|
|
56
|
+
/**
|
|
57
|
+
* Get recent alerts
|
|
58
|
+
*/
|
|
59
|
+
getRecent(limit?: number): Alert[];
|
|
60
|
+
/**
|
|
61
|
+
* Get alert count by type
|
|
62
|
+
*/
|
|
63
|
+
getCounts(): Record<AlertType, number>;
|
|
64
|
+
close(): void;
|
|
65
|
+
private isDuplicate;
|
|
66
|
+
private createAlert;
|
|
67
|
+
private storeAlert;
|
|
68
|
+
private deliverWebhook;
|
|
69
|
+
}
|
|
70
|
+
export declare function getAlertManager(config?: Partial<AlertsConfig>): AlertManager;
|
|
71
|
+
export declare function resetAlertManager(): void;
|
|
72
|
+
//# sourceMappingURL=alerts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alerts.d.ts","sourceRoot":"","sources":["../src/alerts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAIlD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;CACpB;AAID,eAAO,MAAM,qBAAqB,EAAE,YAInC,CAAC;AAyBF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,KAAK,CAAkC;IAE/C,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC;IAI1C,gCAAgC;IAChC,IAAI,IAAI,IAAI;IAkCZ,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,IAAI;IAIjD,SAAS,IAAI,YAAY;IAIzB;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI;IAW3G;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,KAAK,GAAG,IAAI;IAUjD;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI;IAUjF;;OAEG;IACH,SAAS,CAAC,KAAK,GAAE,MAAW,GAAG,KAAK,EAAE;IAkBtC;;OAEG;IACH,SAAS,IAAI,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC;IAetC,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,UAAU;IAqBlB,OAAO,CAAC,cAAc;CA8BvB;AAMD,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAK5E;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
package/dist/alerts.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RelayPlane Cost Alerts & Webhooks
|
|
4
|
+
*
|
|
5
|
+
* Alert types: threshold (budget %), anomaly, breach.
|
|
6
|
+
* Webhook delivery via POST to configured URL.
|
|
7
|
+
* SQLite storage for alert history.
|
|
8
|
+
* Alert deduplication per window.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.AlertManager = exports.DEFAULT_ALERTS_CONFIG = void 0;
|
|
47
|
+
exports.getAlertManager = getAlertManager;
|
|
48
|
+
exports.resetAlertManager = resetAlertManager;
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
50
|
+
const os = __importStar(require("node:os"));
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
// ─── Defaults ────────────────────────────────────────────────────────
|
|
53
|
+
exports.DEFAULT_ALERTS_CONFIG = {
|
|
54
|
+
enabled: false,
|
|
55
|
+
cooldownMs: 300_000,
|
|
56
|
+
maxHistory: 500,
|
|
57
|
+
};
|
|
58
|
+
function openDatabase(dbPath) {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
60
|
+
const Database = require('better-sqlite3');
|
|
61
|
+
const db = new Database(dbPath);
|
|
62
|
+
db.pragma('journal_mode = WAL');
|
|
63
|
+
db.pragma('synchronous = NORMAL');
|
|
64
|
+
return db;
|
|
65
|
+
}
|
|
66
|
+
// ─── AlertManager ───────────────────────────────────────────────────
|
|
67
|
+
class AlertManager {
|
|
68
|
+
config;
|
|
69
|
+
db = null;
|
|
70
|
+
_initialized = false;
|
|
71
|
+
// In-memory deduplication: key → last fired timestamp
|
|
72
|
+
dedup = new Map();
|
|
73
|
+
// In-memory alert history for when DB is unavailable
|
|
74
|
+
memoryAlerts = [];
|
|
75
|
+
alertCounter = 0;
|
|
76
|
+
constructor(config) {
|
|
77
|
+
this.config = { ...exports.DEFAULT_ALERTS_CONFIG, ...config };
|
|
78
|
+
}
|
|
79
|
+
/** Initialize SQLite storage */
|
|
80
|
+
init() {
|
|
81
|
+
if (this._initialized)
|
|
82
|
+
return;
|
|
83
|
+
if (!this.config.enabled)
|
|
84
|
+
return;
|
|
85
|
+
this._initialized = true;
|
|
86
|
+
const dir = path.join(os.homedir(), '.relayplane');
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
try {
|
|
89
|
+
const dbPath = path.join(dir, 'alerts.db');
|
|
90
|
+
this.db = openDatabase(dbPath);
|
|
91
|
+
this.db.exec(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS alerts (
|
|
93
|
+
id TEXT PRIMARY KEY,
|
|
94
|
+
type TEXT NOT NULL,
|
|
95
|
+
message TEXT NOT NULL,
|
|
96
|
+
severity TEXT NOT NULL,
|
|
97
|
+
timestamp INTEGER NOT NULL,
|
|
98
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
99
|
+
delivered INTEGER NOT NULL DEFAULT 0
|
|
100
|
+
);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_alerts_timestamp ON alerts(timestamp);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_alerts_type ON alerts(type);
|
|
103
|
+
`);
|
|
104
|
+
// Prune old alerts
|
|
105
|
+
const cutoff = Date.now() - 7 * 86400_000;
|
|
106
|
+
this.db.prepare('DELETE FROM alerts WHERE timestamp < ?').run(cutoff);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.warn('[RelayPlane Alerts] SQLite unavailable, memory-only mode:', err.message);
|
|
110
|
+
this.db = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
updateConfig(config) {
|
|
114
|
+
this.config = { ...this.config, ...config };
|
|
115
|
+
}
|
|
116
|
+
getConfig() {
|
|
117
|
+
return { ...this.config };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Fire a threshold alert (budget % crossed)
|
|
121
|
+
*/
|
|
122
|
+
fireThreshold(threshold, currentPercent, currentSpend, limit) {
|
|
123
|
+
if (!this.config.enabled)
|
|
124
|
+
return null;
|
|
125
|
+
const dedupKey = `threshold:${threshold}`;
|
|
126
|
+
if (this.isDuplicate(dedupKey))
|
|
127
|
+
return null;
|
|
128
|
+
const severity = threshold >= 95 ? 'critical' : threshold >= 80 ? 'warning' : 'info';
|
|
129
|
+
return this.createAlert('threshold', `Budget ${threshold}% threshold crossed (${currentPercent.toFixed(1)}% used: $${currentSpend.toFixed(2)} / $${limit})`, severity, {
|
|
130
|
+
threshold, currentPercent, currentSpend, limit,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Fire an anomaly alert
|
|
135
|
+
*/
|
|
136
|
+
fireAnomaly(anomaly) {
|
|
137
|
+
if (!this.config.enabled)
|
|
138
|
+
return null;
|
|
139
|
+
const dedupKey = `anomaly:${anomaly.type}`;
|
|
140
|
+
if (this.isDuplicate(dedupKey))
|
|
141
|
+
return null;
|
|
142
|
+
return this.createAlert('anomaly', `Anomaly detected: ${anomaly.message}`, anomaly.severity === 'critical' ? 'critical' : 'warning', {
|
|
143
|
+
anomalyType: anomaly.type, ...anomaly.data,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Fire a breach alert (budget limit exceeded)
|
|
148
|
+
*/
|
|
149
|
+
fireBreach(breachType, currentSpend, limit) {
|
|
150
|
+
if (!this.config.enabled)
|
|
151
|
+
return null;
|
|
152
|
+
const dedupKey = `breach:${breachType}`;
|
|
153
|
+
if (this.isDuplicate(dedupKey))
|
|
154
|
+
return null;
|
|
155
|
+
return this.createAlert('breach', `Budget breach: ${breachType} limit exceeded ($${currentSpend.toFixed(2)} / $${limit})`, 'critical', {
|
|
156
|
+
breachType, currentSpend, limit,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get recent alerts
|
|
161
|
+
*/
|
|
162
|
+
getRecent(limit = 20) {
|
|
163
|
+
if (this.db) {
|
|
164
|
+
const rows = this.db.prepare('SELECT id, type, message, severity, timestamp, data, delivered FROM alerts ORDER BY timestamp DESC LIMIT ?').all(limit);
|
|
165
|
+
return rows.map(r => ({
|
|
166
|
+
id: r.id,
|
|
167
|
+
type: r.type,
|
|
168
|
+
message: r.message,
|
|
169
|
+
severity: r.severity,
|
|
170
|
+
timestamp: r.timestamp,
|
|
171
|
+
data: JSON.parse(r.data),
|
|
172
|
+
delivered: r.delivered === 1,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
return this.memoryAlerts.slice(-limit).reverse();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get alert count by type
|
|
179
|
+
*/
|
|
180
|
+
getCounts() {
|
|
181
|
+
const counts = { threshold: 0, anomaly: 0, breach: 0 };
|
|
182
|
+
if (this.db) {
|
|
183
|
+
const rows = this.db.prepare('SELECT type, COUNT(*) as c FROM alerts GROUP BY type').all();
|
|
184
|
+
for (const r of rows) {
|
|
185
|
+
if (r.type in counts)
|
|
186
|
+
counts[r.type] = r.c;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
for (const a of this.memoryAlerts) {
|
|
191
|
+
counts[a.type]++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return counts;
|
|
195
|
+
}
|
|
196
|
+
close() {
|
|
197
|
+
if (this.db) {
|
|
198
|
+
this.db.close();
|
|
199
|
+
this.db = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ─── Private ──────────────────────────────────────────────────────
|
|
203
|
+
isDuplicate(key) {
|
|
204
|
+
const last = this.dedup.get(key);
|
|
205
|
+
if (last && (Date.now() - last) < this.config.cooldownMs) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
this.dedup.set(key, Date.now());
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
createAlert(type, message, severity, data) {
|
|
212
|
+
const alert = {
|
|
213
|
+
id: `alert-${++this.alertCounter}-${Date.now()}`,
|
|
214
|
+
type,
|
|
215
|
+
message,
|
|
216
|
+
severity,
|
|
217
|
+
timestamp: Date.now(),
|
|
218
|
+
data,
|
|
219
|
+
delivered: false,
|
|
220
|
+
};
|
|
221
|
+
// Store
|
|
222
|
+
this.storeAlert(alert);
|
|
223
|
+
// Deliver webhook (non-blocking)
|
|
224
|
+
this.deliverWebhook(alert);
|
|
225
|
+
return alert;
|
|
226
|
+
}
|
|
227
|
+
storeAlert(alert) {
|
|
228
|
+
if (this.db) {
|
|
229
|
+
this.db.prepare('INSERT INTO alerts (id, type, message, severity, timestamp, data, delivered) VALUES (?, ?, ?, ?, ?, ?, ?)').run(alert.id, alert.type, alert.message, alert.severity, alert.timestamp, JSON.stringify(alert.data), alert.delivered ? 1 : 0);
|
|
230
|
+
// Prune excess
|
|
231
|
+
const count = this.db.prepare('SELECT COUNT(*) as c FROM alerts').get().c;
|
|
232
|
+
if (count > this.config.maxHistory) {
|
|
233
|
+
this.db.prepare('DELETE FROM alerts WHERE id IN (SELECT id FROM alerts ORDER BY timestamp ASC LIMIT ?)').run(count - this.config.maxHistory);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this.memoryAlerts.push(alert);
|
|
238
|
+
if (this.memoryAlerts.length > this.config.maxHistory) {
|
|
239
|
+
this.memoryAlerts.shift();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
deliverWebhook(alert) {
|
|
244
|
+
if (!this.config.webhookUrl)
|
|
245
|
+
return;
|
|
246
|
+
const url = this.config.webhookUrl;
|
|
247
|
+
// Fire and forget
|
|
248
|
+
fetch(url, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
headers: { 'Content-Type': 'application/json' },
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
source: 'relayplane',
|
|
253
|
+
alert: {
|
|
254
|
+
id: alert.id,
|
|
255
|
+
type: alert.type,
|
|
256
|
+
message: alert.message,
|
|
257
|
+
severity: alert.severity,
|
|
258
|
+
timestamp: new Date(alert.timestamp).toISOString(),
|
|
259
|
+
data: alert.data,
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
}).then(() => {
|
|
263
|
+
alert.delivered = true;
|
|
264
|
+
try {
|
|
265
|
+
if (this.db) {
|
|
266
|
+
this.db.prepare('UPDATE alerts SET delivered = 1 WHERE id = ?').run(alert.id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch { /* SQLite failure non-fatal */ }
|
|
270
|
+
}).catch(() => {
|
|
271
|
+
// Webhook delivery failure — non-fatal
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
exports.AlertManager = AlertManager;
|
|
276
|
+
// ─── Singleton ──────────────────────────────────────────────────────
|
|
277
|
+
let _instance = null;
|
|
278
|
+
function getAlertManager(config) {
|
|
279
|
+
if (!_instance) {
|
|
280
|
+
_instance = new AlertManager(config);
|
|
281
|
+
}
|
|
282
|
+
return _instance;
|
|
283
|
+
}
|
|
284
|
+
function resetAlertManager() {
|
|
285
|
+
if (_instance) {
|
|
286
|
+
_instance.close();
|
|
287
|
+
_instance = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=alerts.js.map
|