@relayplane/proxy 0.1.7 → 0.1.8
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 +73 -1
- package/dist/cli.js +126 -15
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +126 -15
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +96 -1
- package/dist/index.d.ts +96 -1
- package/dist/index.js +181 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +175 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -176,13 +176,85 @@ Options:
|
|
|
176
176
|
-h, --help Show help
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
## REST API
|
|
180
|
+
|
|
181
|
+
The proxy exposes endpoints for stats and monitoring:
|
|
182
|
+
|
|
183
|
+
### `GET /health`
|
|
184
|
+
|
|
185
|
+
Server health and version info.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
curl http://localhost:3001/health
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"status": "ok",
|
|
194
|
+
"version": "0.1.7",
|
|
195
|
+
"uptime": "2h 15m 30s",
|
|
196
|
+
"providers": { "anthropic": true, "openai": true, "google": false },
|
|
197
|
+
"totalRuns": 142
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `GET /stats`
|
|
202
|
+
|
|
203
|
+
Aggregated statistics and cost savings.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
curl http://localhost:3001/stats
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"totalRuns": 142,
|
|
212
|
+
"savings": {
|
|
213
|
+
"estimatedSavingsPercent": "73.2%",
|
|
214
|
+
"actualCostUsd": "0.0234",
|
|
215
|
+
"baselineCostUsd": "0.0873",
|
|
216
|
+
"savedUsd": "0.0639"
|
|
217
|
+
},
|
|
218
|
+
"modelDistribution": {
|
|
219
|
+
"anthropic/claude-3-5-haiku-latest": { "count": 98, "percentage": "69.0%" },
|
|
220
|
+
"anthropic/claude-sonnet-4-20250514": { "count": 44, "percentage": "31.0%" }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `GET /runs`
|
|
226
|
+
|
|
227
|
+
Recent routing decisions.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
curl "http://localhost:3001/runs?limit=10"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"runs": [
|
|
236
|
+
{
|
|
237
|
+
"runId": "abc123",
|
|
238
|
+
"timestamp": "2026-02-03T13:26:03Z",
|
|
239
|
+
"model": "anthropic/claude-3-5-haiku-latest",
|
|
240
|
+
"taskType": "code_generation",
|
|
241
|
+
"confidence": 0.92,
|
|
242
|
+
"mode": "auto",
|
|
243
|
+
"durationMs": 1203,
|
|
244
|
+
"promptPreview": "Write a function that..."
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
"total": 142
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
179
251
|
## Data Storage
|
|
180
252
|
|
|
181
253
|
All data stored locally at `~/.relayplane/data.db` (SQLite).
|
|
182
254
|
|
|
183
255
|
```bash
|
|
184
256
|
# View recent runs
|
|
185
|
-
sqlite3 ~/.relayplane/data.db "SELECT * FROM runs ORDER BY
|
|
257
|
+
sqlite3 ~/.relayplane/data.db "SELECT * FROM runs ORDER BY created_at DESC LIMIT 10"
|
|
186
258
|
|
|
187
259
|
# Check routing rules
|
|
188
260
|
sqlite3 ~/.relayplane/data.db "SELECT * FROM routing_rules"
|
package/dist/cli.js
CHANGED
|
@@ -1591,12 +1591,107 @@ ${input.prompt}` : input.prompt;
|
|
|
1591
1591
|
}
|
|
1592
1592
|
};
|
|
1593
1593
|
|
|
1594
|
+
// src/config.ts
|
|
1595
|
+
var fs2 = __toESM(require("fs"));
|
|
1596
|
+
var path2 = __toESM(require("path"));
|
|
1597
|
+
var os2 = __toESM(require("os"));
|
|
1598
|
+
var import_zod = require("zod");
|
|
1599
|
+
var StrategySchema = import_zod.z.object({
|
|
1600
|
+
model: import_zod.z.string(),
|
|
1601
|
+
minConfidence: import_zod.z.number().min(0).max(1).optional(),
|
|
1602
|
+
fallback: import_zod.z.string().optional()
|
|
1603
|
+
});
|
|
1604
|
+
var ConfigSchema = import_zod.z.object({
|
|
1605
|
+
strategies: import_zod.z.record(import_zod.z.string(), StrategySchema).optional(),
|
|
1606
|
+
defaults: import_zod.z.object({
|
|
1607
|
+
qualityModel: import_zod.z.string().optional(),
|
|
1608
|
+
costModel: import_zod.z.string().optional()
|
|
1609
|
+
}).optional()
|
|
1610
|
+
});
|
|
1611
|
+
var DEFAULT_CONFIG = {
|
|
1612
|
+
strategies: {
|
|
1613
|
+
code_review: { model: "anthropic:claude-sonnet-4-20250514" },
|
|
1614
|
+
code_generation: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1615
|
+
analysis: { model: "anthropic:claude-sonnet-4-20250514" },
|
|
1616
|
+
summarization: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1617
|
+
creative_writing: { model: "anthropic:claude-sonnet-4-20250514" },
|
|
1618
|
+
data_extraction: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1619
|
+
translation: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1620
|
+
question_answering: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1621
|
+
general: { model: "anthropic:claude-3-5-haiku-latest" }
|
|
1622
|
+
},
|
|
1623
|
+
defaults: {
|
|
1624
|
+
qualityModel: "claude-sonnet-4-20250514",
|
|
1625
|
+
costModel: "claude-3-5-haiku-latest"
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
function getConfigPath() {
|
|
1629
|
+
return path2.join(os2.homedir(), ".relayplane", "config.json");
|
|
1630
|
+
}
|
|
1631
|
+
function writeDefaultConfig() {
|
|
1632
|
+
const configPath = getConfigPath();
|
|
1633
|
+
const dir = path2.dirname(configPath);
|
|
1634
|
+
if (!fs2.existsSync(dir)) {
|
|
1635
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1636
|
+
}
|
|
1637
|
+
if (!fs2.existsSync(configPath)) {
|
|
1638
|
+
fs2.writeFileSync(
|
|
1639
|
+
configPath,
|
|
1640
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n",
|
|
1641
|
+
"utf-8"
|
|
1642
|
+
);
|
|
1643
|
+
console.log(`[relayplane] Created default config at ${configPath}`);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function loadConfig() {
|
|
1647
|
+
const configPath = getConfigPath();
|
|
1648
|
+
writeDefaultConfig();
|
|
1649
|
+
try {
|
|
1650
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
1651
|
+
const parsed = JSON.parse(raw);
|
|
1652
|
+
const validated = ConfigSchema.parse(parsed);
|
|
1653
|
+
return validated;
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
if (err instanceof import_zod.z.ZodError) {
|
|
1656
|
+
console.error(`[relayplane] Invalid config: ${err.message}`);
|
|
1657
|
+
} else if (err instanceof SyntaxError) {
|
|
1658
|
+
console.error(`[relayplane] Config JSON parse error: ${err.message}`);
|
|
1659
|
+
} else {
|
|
1660
|
+
console.error(`[relayplane] Failed to load config: ${err}`);
|
|
1661
|
+
}
|
|
1662
|
+
console.log("[relayplane] Using default config");
|
|
1663
|
+
return DEFAULT_CONFIG;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function getStrategy(config, taskType) {
|
|
1667
|
+
return config.strategies?.[taskType] ?? null;
|
|
1668
|
+
}
|
|
1669
|
+
function watchConfig(onChange) {
|
|
1670
|
+
const configPath = getConfigPath();
|
|
1671
|
+
const dir = path2.dirname(configPath);
|
|
1672
|
+
if (!fs2.existsSync(dir)) {
|
|
1673
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1674
|
+
}
|
|
1675
|
+
let debounceTimer = null;
|
|
1676
|
+
fs2.watch(dir, (eventType, filename) => {
|
|
1677
|
+
if (filename === "config.json") {
|
|
1678
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1679
|
+
debounceTimer = setTimeout(() => {
|
|
1680
|
+
console.log("[relayplane] Config file changed, reloading...");
|
|
1681
|
+
const newConfig = loadConfig();
|
|
1682
|
+
onChange(newConfig);
|
|
1683
|
+
}, 100);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1594
1688
|
// src/proxy.ts
|
|
1595
|
-
var VERSION = "0.1.
|
|
1689
|
+
var VERSION = "0.1.8";
|
|
1596
1690
|
var recentRuns = [];
|
|
1597
1691
|
var MAX_RECENT_RUNS = 100;
|
|
1598
1692
|
var modelCounts = {};
|
|
1599
1693
|
var serverStartTime = 0;
|
|
1694
|
+
var currentConfig = loadConfig();
|
|
1600
1695
|
var DEFAULT_ENDPOINTS = {
|
|
1601
1696
|
anthropic: {
|
|
1602
1697
|
baseUrl: "https://api.anthropic.com/v1",
|
|
@@ -2447,33 +2542,44 @@ async function startProxy(config = {}) {
|
|
|
2447
2542
|
const confidence = getInferenceConfidence(promptText, taskType);
|
|
2448
2543
|
log(`Inferred task: ${taskType} (confidence: ${confidence.toFixed(2)})`);
|
|
2449
2544
|
if (routingMode !== "passthrough") {
|
|
2450
|
-
const
|
|
2451
|
-
if (
|
|
2452
|
-
const parsed = parsePreferredModel(
|
|
2545
|
+
const configStrategy = getStrategy(currentConfig, taskType);
|
|
2546
|
+
if (configStrategy) {
|
|
2547
|
+
const parsed = parsePreferredModel(configStrategy.model);
|
|
2453
2548
|
if (parsed) {
|
|
2454
2549
|
targetProvider = parsed.provider;
|
|
2455
2550
|
targetModel = parsed.model;
|
|
2456
|
-
log(`Using
|
|
2551
|
+
log(`Using config strategy: ${configStrategy.model}`);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
if (!configStrategy) {
|
|
2555
|
+
const rule = relay.routing.get(taskType);
|
|
2556
|
+
if (rule && rule.preferredModel) {
|
|
2557
|
+
const parsed = parsePreferredModel(rule.preferredModel);
|
|
2558
|
+
if (parsed) {
|
|
2559
|
+
targetProvider = parsed.provider;
|
|
2560
|
+
targetModel = parsed.model;
|
|
2561
|
+
log(`Using learned rule: ${rule.preferredModel}`);
|
|
2562
|
+
} else {
|
|
2563
|
+
const defaultRoute = DEFAULT_ROUTING[taskType];
|
|
2564
|
+
targetProvider = defaultRoute.provider;
|
|
2565
|
+
targetModel = defaultRoute.model;
|
|
2566
|
+
}
|
|
2457
2567
|
} else {
|
|
2458
2568
|
const defaultRoute = DEFAULT_ROUTING[taskType];
|
|
2459
2569
|
targetProvider = defaultRoute.provider;
|
|
2460
2570
|
targetModel = defaultRoute.model;
|
|
2461
2571
|
}
|
|
2462
|
-
} else {
|
|
2463
|
-
const defaultRoute = DEFAULT_ROUTING[taskType];
|
|
2464
|
-
targetProvider = defaultRoute.provider;
|
|
2465
|
-
targetModel = defaultRoute.model;
|
|
2466
2572
|
}
|
|
2467
2573
|
if (routingMode === "cost") {
|
|
2468
|
-
const
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
}
|
|
2574
|
+
const costModel = currentConfig.defaults?.costModel || "claude-3-5-haiku-latest";
|
|
2575
|
+
targetModel = costModel;
|
|
2576
|
+
targetProvider = "anthropic";
|
|
2577
|
+
log(`Cost mode: using ${costModel}`);
|
|
2473
2578
|
} else if (routingMode === "quality") {
|
|
2474
|
-
const qualityModel = process.env["RELAYPLANE_QUALITY_MODEL"] || "claude-sonnet-4-20250514";
|
|
2579
|
+
const qualityModel = currentConfig.defaults?.qualityModel || process.env["RELAYPLANE_QUALITY_MODEL"] || "claude-sonnet-4-20250514";
|
|
2475
2580
|
targetModel = qualityModel;
|
|
2476
2581
|
targetProvider = "anthropic";
|
|
2582
|
+
log(`Quality mode: using ${qualityModel}`);
|
|
2477
2583
|
}
|
|
2478
2584
|
}
|
|
2479
2585
|
log(`Routing to: ${targetProvider}/${targetModel}`);
|
|
@@ -2520,6 +2626,10 @@ async function startProxy(config = {}) {
|
|
|
2520
2626
|
);
|
|
2521
2627
|
}
|
|
2522
2628
|
});
|
|
2629
|
+
watchConfig((newConfig) => {
|
|
2630
|
+
currentConfig = newConfig;
|
|
2631
|
+
console.log("[relayplane] Config reloaded");
|
|
2632
|
+
});
|
|
2523
2633
|
return new Promise((resolve, reject) => {
|
|
2524
2634
|
server.on("error", reject);
|
|
2525
2635
|
server.listen(port, host, () => {
|
|
@@ -2528,6 +2638,7 @@ async function startProxy(config = {}) {
|
|
|
2528
2638
|
console.log(` Models: relayplane:auto, relayplane:cost, relayplane:quality`);
|
|
2529
2639
|
console.log(` Endpoint: POST /v1/chat/completions`);
|
|
2530
2640
|
console.log(` Stats: GET /stats, /runs, /health`);
|
|
2641
|
+
console.log(` Config: ~/.relayplane/config.json (hot-reload enabled)`);
|
|
2531
2642
|
console.log(` Streaming: \u2705 Enabled`);
|
|
2532
2643
|
resolve(server);
|
|
2533
2644
|
});
|