@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 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 timestamp DESC LIMIT 10"
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.7";
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 rule = relay.routing.get(taskType);
2451
- if (rule && rule.preferredModel) {
2452
- const parsed = parsePreferredModel(rule.preferredModel);
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 learned rule: ${rule.preferredModel}`);
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 simpleTasks = ["summarization", "data_extraction", "translation", "question_answering"];
2469
- if (simpleTasks.includes(taskType)) {
2470
- targetModel = "claude-3-5-haiku-latest";
2471
- targetProvider = "anthropic";
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
  });