@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/dist/cli.mjs CHANGED
@@ -1568,12 +1568,107 @@ ${input.prompt}` : input.prompt;
1568
1568
  }
1569
1569
  };
1570
1570
 
1571
+ // src/config.ts
1572
+ import * as fs2 from "fs";
1573
+ import * as path2 from "path";
1574
+ import * as os2 from "os";
1575
+ import { z } from "zod";
1576
+ var StrategySchema = z.object({
1577
+ model: z.string(),
1578
+ minConfidence: z.number().min(0).max(1).optional(),
1579
+ fallback: z.string().optional()
1580
+ });
1581
+ var ConfigSchema = z.object({
1582
+ strategies: z.record(z.string(), StrategySchema).optional(),
1583
+ defaults: z.object({
1584
+ qualityModel: z.string().optional(),
1585
+ costModel: z.string().optional()
1586
+ }).optional()
1587
+ });
1588
+ var DEFAULT_CONFIG = {
1589
+ strategies: {
1590
+ code_review: { model: "anthropic:claude-sonnet-4-20250514" },
1591
+ code_generation: { model: "anthropic:claude-3-5-haiku-latest" },
1592
+ analysis: { model: "anthropic:claude-sonnet-4-20250514" },
1593
+ summarization: { model: "anthropic:claude-3-5-haiku-latest" },
1594
+ creative_writing: { model: "anthropic:claude-sonnet-4-20250514" },
1595
+ data_extraction: { model: "anthropic:claude-3-5-haiku-latest" },
1596
+ translation: { model: "anthropic:claude-3-5-haiku-latest" },
1597
+ question_answering: { model: "anthropic:claude-3-5-haiku-latest" },
1598
+ general: { model: "anthropic:claude-3-5-haiku-latest" }
1599
+ },
1600
+ defaults: {
1601
+ qualityModel: "claude-sonnet-4-20250514",
1602
+ costModel: "claude-3-5-haiku-latest"
1603
+ }
1604
+ };
1605
+ function getConfigPath() {
1606
+ return path2.join(os2.homedir(), ".relayplane", "config.json");
1607
+ }
1608
+ function writeDefaultConfig() {
1609
+ const configPath = getConfigPath();
1610
+ const dir = path2.dirname(configPath);
1611
+ if (!fs2.existsSync(dir)) {
1612
+ fs2.mkdirSync(dir, { recursive: true });
1613
+ }
1614
+ if (!fs2.existsSync(configPath)) {
1615
+ fs2.writeFileSync(
1616
+ configPath,
1617
+ JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n",
1618
+ "utf-8"
1619
+ );
1620
+ console.log(`[relayplane] Created default config at ${configPath}`);
1621
+ }
1622
+ }
1623
+ function loadConfig() {
1624
+ const configPath = getConfigPath();
1625
+ writeDefaultConfig();
1626
+ try {
1627
+ const raw = fs2.readFileSync(configPath, "utf-8");
1628
+ const parsed = JSON.parse(raw);
1629
+ const validated = ConfigSchema.parse(parsed);
1630
+ return validated;
1631
+ } catch (err) {
1632
+ if (err instanceof z.ZodError) {
1633
+ console.error(`[relayplane] Invalid config: ${err.message}`);
1634
+ } else if (err instanceof SyntaxError) {
1635
+ console.error(`[relayplane] Config JSON parse error: ${err.message}`);
1636
+ } else {
1637
+ console.error(`[relayplane] Failed to load config: ${err}`);
1638
+ }
1639
+ console.log("[relayplane] Using default config");
1640
+ return DEFAULT_CONFIG;
1641
+ }
1642
+ }
1643
+ function getStrategy(config, taskType) {
1644
+ return config.strategies?.[taskType] ?? null;
1645
+ }
1646
+ function watchConfig(onChange) {
1647
+ const configPath = getConfigPath();
1648
+ const dir = path2.dirname(configPath);
1649
+ if (!fs2.existsSync(dir)) {
1650
+ fs2.mkdirSync(dir, { recursive: true });
1651
+ }
1652
+ let debounceTimer = null;
1653
+ fs2.watch(dir, (eventType, filename) => {
1654
+ if (filename === "config.json") {
1655
+ if (debounceTimer) clearTimeout(debounceTimer);
1656
+ debounceTimer = setTimeout(() => {
1657
+ console.log("[relayplane] Config file changed, reloading...");
1658
+ const newConfig = loadConfig();
1659
+ onChange(newConfig);
1660
+ }, 100);
1661
+ }
1662
+ });
1663
+ }
1664
+
1571
1665
  // src/proxy.ts
1572
- var VERSION = "0.1.7";
1666
+ var VERSION = "0.1.8";
1573
1667
  var recentRuns = [];
1574
1668
  var MAX_RECENT_RUNS = 100;
1575
1669
  var modelCounts = {};
1576
1670
  var serverStartTime = 0;
1671
+ var currentConfig = loadConfig();
1577
1672
  var DEFAULT_ENDPOINTS = {
1578
1673
  anthropic: {
1579
1674
  baseUrl: "https://api.anthropic.com/v1",
@@ -2424,33 +2519,44 @@ async function startProxy(config = {}) {
2424
2519
  const confidence = getInferenceConfidence(promptText, taskType);
2425
2520
  log(`Inferred task: ${taskType} (confidence: ${confidence.toFixed(2)})`);
2426
2521
  if (routingMode !== "passthrough") {
2427
- const rule = relay.routing.get(taskType);
2428
- if (rule && rule.preferredModel) {
2429
- const parsed = parsePreferredModel(rule.preferredModel);
2522
+ const configStrategy = getStrategy(currentConfig, taskType);
2523
+ if (configStrategy) {
2524
+ const parsed = parsePreferredModel(configStrategy.model);
2430
2525
  if (parsed) {
2431
2526
  targetProvider = parsed.provider;
2432
2527
  targetModel = parsed.model;
2433
- log(`Using learned rule: ${rule.preferredModel}`);
2528
+ log(`Using config strategy: ${configStrategy.model}`);
2529
+ }
2530
+ }
2531
+ if (!configStrategy) {
2532
+ const rule = relay.routing.get(taskType);
2533
+ if (rule && rule.preferredModel) {
2534
+ const parsed = parsePreferredModel(rule.preferredModel);
2535
+ if (parsed) {
2536
+ targetProvider = parsed.provider;
2537
+ targetModel = parsed.model;
2538
+ log(`Using learned rule: ${rule.preferredModel}`);
2539
+ } else {
2540
+ const defaultRoute = DEFAULT_ROUTING[taskType];
2541
+ targetProvider = defaultRoute.provider;
2542
+ targetModel = defaultRoute.model;
2543
+ }
2434
2544
  } else {
2435
2545
  const defaultRoute = DEFAULT_ROUTING[taskType];
2436
2546
  targetProvider = defaultRoute.provider;
2437
2547
  targetModel = defaultRoute.model;
2438
2548
  }
2439
- } else {
2440
- const defaultRoute = DEFAULT_ROUTING[taskType];
2441
- targetProvider = defaultRoute.provider;
2442
- targetModel = defaultRoute.model;
2443
2549
  }
2444
2550
  if (routingMode === "cost") {
2445
- const simpleTasks = ["summarization", "data_extraction", "translation", "question_answering"];
2446
- if (simpleTasks.includes(taskType)) {
2447
- targetModel = "claude-3-5-haiku-latest";
2448
- targetProvider = "anthropic";
2449
- }
2551
+ const costModel = currentConfig.defaults?.costModel || "claude-3-5-haiku-latest";
2552
+ targetModel = costModel;
2553
+ targetProvider = "anthropic";
2554
+ log(`Cost mode: using ${costModel}`);
2450
2555
  } else if (routingMode === "quality") {
2451
- const qualityModel = process.env["RELAYPLANE_QUALITY_MODEL"] || "claude-sonnet-4-20250514";
2556
+ const qualityModel = currentConfig.defaults?.qualityModel || process.env["RELAYPLANE_QUALITY_MODEL"] || "claude-sonnet-4-20250514";
2452
2557
  targetModel = qualityModel;
2453
2558
  targetProvider = "anthropic";
2559
+ log(`Quality mode: using ${qualityModel}`);
2454
2560
  }
2455
2561
  }
2456
2562
  log(`Routing to: ${targetProvider}/${targetModel}`);
@@ -2497,6 +2603,10 @@ async function startProxy(config = {}) {
2497
2603
  );
2498
2604
  }
2499
2605
  });
2606
+ watchConfig((newConfig) => {
2607
+ currentConfig = newConfig;
2608
+ console.log("[relayplane] Config reloaded");
2609
+ });
2500
2610
  return new Promise((resolve, reject) => {
2501
2611
  server.on("error", reject);
2502
2612
  server.listen(port, host, () => {
@@ -2505,6 +2615,7 @@ async function startProxy(config = {}) {
2505
2615
  console.log(` Models: relayplane:auto, relayplane:cost, relayplane:quality`);
2506
2616
  console.log(` Endpoint: POST /v1/chat/completions`);
2507
2617
  console.log(` Stats: GET /stats, /runs, /health`);
2618
+ console.log(` Config: ~/.relayplane/config.json (hot-reload enabled)`);
2508
2619
  console.log(` Streaming: \u2705 Enabled`);
2509
2620
  resolve(server);
2510
2621
  });