@relayplane/proxy 0.1.8 → 0.1.9

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
@@ -248,6 +248,55 @@ curl "http://localhost:3001/runs?limit=10"
248
248
  }
249
249
  ```
250
250
 
251
+ ## Configuration
252
+
253
+ RelayPlane creates a config file on first run at `~/.relayplane/config.json`:
254
+
255
+ ```json
256
+ {
257
+ "strategies": {
258
+ "code_review": { "model": "anthropic:claude-sonnet-4-20250514" },
259
+ "code_generation": { "model": "anthropic:claude-3-5-haiku-latest" },
260
+ "analysis": { "model": "anthropic:claude-sonnet-4-20250514" },
261
+ "summarization": { "model": "anthropic:claude-3-5-haiku-latest" },
262
+ "creative_writing": { "model": "anthropic:claude-sonnet-4-20250514" },
263
+ "data_extraction": { "model": "anthropic:claude-3-5-haiku-latest" },
264
+ "translation": { "model": "anthropic:claude-3-5-haiku-latest" },
265
+ "question_answering": { "model": "anthropic:claude-3-5-haiku-latest" },
266
+ "general": { "model": "anthropic:claude-3-5-haiku-latest" }
267
+ },
268
+ "defaults": {
269
+ "qualityModel": "claude-sonnet-4-20250514",
270
+ "costModel": "claude-3-5-haiku-latest"
271
+ }
272
+ }
273
+ ```
274
+
275
+ **Edit and save — changes apply instantly** (hot-reload, no restart needed).
276
+
277
+ ### Strategy Options
278
+
279
+ | Field | Description |
280
+ |-------|-------------|
281
+ | `model` | Provider and model in format `provider:model` |
282
+ | `minConfidence` | Optional. Only use this strategy if confidence >= threshold |
283
+ | `fallback` | Optional. Fallback model if primary fails |
284
+
285
+ ### Examples
286
+
287
+ Route all analysis tasks to GPT-4o:
288
+ ```json
289
+ "analysis": { "model": "openai:gpt-4o" }
290
+ ```
291
+
292
+ Use Opus for code review with fallback:
293
+ ```json
294
+ "code_review": {
295
+ "model": "anthropic:claude-opus-4-5-20250514",
296
+ "fallback": "anthropic:claude-sonnet-4-20250514"
297
+ }
298
+ ```
299
+
251
300
  ## Data Storage
252
301
 
253
302
  All data stored locally at `~/.relayplane/data.db` (SQLite).
package/dist/cli.js CHANGED
@@ -1601,12 +1601,19 @@ var StrategySchema = import_zod.z.object({
1601
1601
  minConfidence: import_zod.z.number().min(0).max(1).optional(),
1602
1602
  fallback: import_zod.z.string().optional()
1603
1603
  });
1604
+ var AuthSchema = import_zod.z.object({
1605
+ anthropicApiKey: import_zod.z.string().optional(),
1606
+ anthropicMaxToken: import_zod.z.string().optional(),
1607
+ useMaxForModels: import_zod.z.array(import_zod.z.string()).optional()
1608
+ // Default: ['opus']
1609
+ }).optional();
1604
1610
  var ConfigSchema = import_zod.z.object({
1605
1611
  strategies: import_zod.z.record(import_zod.z.string(), StrategySchema).optional(),
1606
1612
  defaults: import_zod.z.object({
1607
1613
  qualityModel: import_zod.z.string().optional(),
1608
1614
  costModel: import_zod.z.string().optional()
1609
- }).optional()
1615
+ }).optional(),
1616
+ auth: AuthSchema
1610
1617
  });
1611
1618
  var DEFAULT_CONFIG = {
1612
1619
  strategies: {
@@ -1666,6 +1673,19 @@ function loadConfig() {
1666
1673
  function getStrategy(config, taskType) {
1667
1674
  return config.strategies?.[taskType] ?? null;
1668
1675
  }
1676
+ function getAnthropicAuth(config, model) {
1677
+ const auth = config.auth;
1678
+ const useMaxForModels = auth?.useMaxForModels ?? ["opus"];
1679
+ const shouldUseMax = useMaxForModels.some((m) => model.toLowerCase().includes(m.toLowerCase()));
1680
+ if (shouldUseMax && auth?.anthropicMaxToken) {
1681
+ return { type: "max", value: auth.anthropicMaxToken };
1682
+ }
1683
+ const apiKey = auth?.anthropicApiKey ?? process.env["ANTHROPIC_API_KEY"];
1684
+ if (apiKey) {
1685
+ return { type: "apiKey", value: apiKey };
1686
+ }
1687
+ return null;
1688
+ }
1669
1689
  function watchConfig(onChange) {
1670
1690
  const configPath = getConfigPath();
1671
1691
  const dir = path2.dirname(configPath);
@@ -1686,7 +1706,7 @@ function watchConfig(onChange) {
1686
1706
  }
1687
1707
 
1688
1708
  // src/proxy.ts
1689
- var VERSION = "0.1.8";
1709
+ var VERSION = "0.1.9";
1690
1710
  var recentRuns = [];
1691
1711
  var MAX_RECENT_RUNS = 100;
1692
1712
  var modelCounts = {};
@@ -1754,13 +1774,17 @@ function extractPromptText(messages) {
1754
1774
  return "";
1755
1775
  }).join("\n");
1756
1776
  }
1757
- async function forwardToAnthropic(request, targetModel, apiKey, betaHeaders) {
1777
+ async function forwardToAnthropic(request, targetModel, auth, betaHeaders) {
1758
1778
  const anthropicBody = buildAnthropicBody(request, targetModel, false);
1759
1779
  const headers = {
1760
1780
  "Content-Type": "application/json",
1761
- "x-api-key": apiKey,
1762
1781
  "anthropic-version": "2023-06-01"
1763
1782
  };
1783
+ if (auth.type === "max") {
1784
+ headers["Authorization"] = `Bearer ${auth.value}`;
1785
+ } else {
1786
+ headers["x-api-key"] = auth.value;
1787
+ }
1764
1788
  if (betaHeaders) {
1765
1789
  headers["anthropic-beta"] = betaHeaders;
1766
1790
  }
@@ -1771,13 +1795,17 @@ async function forwardToAnthropic(request, targetModel, apiKey, betaHeaders) {
1771
1795
  });
1772
1796
  return response;
1773
1797
  }
1774
- async function forwardToAnthropicStream(request, targetModel, apiKey, betaHeaders) {
1798
+ async function forwardToAnthropicStream(request, targetModel, auth, betaHeaders) {
1775
1799
  const anthropicBody = buildAnthropicBody(request, targetModel, true);
1776
1800
  const headers = {
1777
1801
  "Content-Type": "application/json",
1778
- "x-api-key": apiKey,
1779
1802
  "anthropic-version": "2023-06-01"
1780
1803
  };
1804
+ if (auth.type === "max") {
1805
+ headers["Authorization"] = `Bearer ${auth.value}`;
1806
+ } else {
1807
+ headers["x-api-key"] = auth.value;
1808
+ }
1781
1809
  if (betaHeaders) {
1782
1810
  headers["anthropic-beta"] = betaHeaders;
1783
1811
  }
@@ -2583,12 +2611,24 @@ async function startProxy(config = {}) {
2583
2611
  }
2584
2612
  }
2585
2613
  log(`Routing to: ${targetProvider}/${targetModel}`);
2586
- const apiKeyEnv = DEFAULT_ENDPOINTS[targetProvider]?.apiKeyEnv ?? `${targetProvider.toUpperCase()}_API_KEY`;
2587
- const apiKey = process.env[apiKeyEnv];
2588
- if (!apiKey) {
2589
- res.writeHead(500, { "Content-Type": "application/json" });
2590
- res.end(JSON.stringify({ error: `Missing ${apiKeyEnv} environment variable` }));
2591
- return;
2614
+ let apiKey;
2615
+ let anthropicAuth = null;
2616
+ if (targetProvider === "anthropic") {
2617
+ anthropicAuth = getAnthropicAuth(currentConfig, targetModel);
2618
+ if (!anthropicAuth) {
2619
+ res.writeHead(500, { "Content-Type": "application/json" });
2620
+ res.end(JSON.stringify({ error: "No Anthropic auth configured (set ANTHROPIC_API_KEY or config.auth.anthropicMaxToken)" }));
2621
+ return;
2622
+ }
2623
+ log(`Using ${anthropicAuth.type === "max" ? "MAX token" : "API key"} auth for ${targetModel}`);
2624
+ } else {
2625
+ const apiKeyEnv = DEFAULT_ENDPOINTS[targetProvider]?.apiKeyEnv ?? `${targetProvider.toUpperCase()}_API_KEY`;
2626
+ apiKey = process.env[apiKeyEnv];
2627
+ if (!apiKey) {
2628
+ res.writeHead(500, { "Content-Type": "application/json" });
2629
+ res.end(JSON.stringify({ error: `Missing ${apiKeyEnv} environment variable` }));
2630
+ return;
2631
+ }
2592
2632
  }
2593
2633
  const startTime = Date.now();
2594
2634
  const betaHeaders = req.headers["anthropic-beta"];
@@ -2599,6 +2639,7 @@ async function startProxy(config = {}) {
2599
2639
  targetProvider,
2600
2640
  targetModel,
2601
2641
  apiKey,
2642
+ anthropicAuth,
2602
2643
  relay,
2603
2644
  promptText,
2604
2645
  taskType,
@@ -2615,6 +2656,7 @@ async function startProxy(config = {}) {
2615
2656
  targetProvider,
2616
2657
  targetModel,
2617
2658
  apiKey,
2659
+ anthropicAuth,
2618
2660
  relay,
2619
2661
  promptText,
2620
2662
  taskType,
@@ -2644,12 +2686,13 @@ async function startProxy(config = {}) {
2644
2686
  });
2645
2687
  });
2646
2688
  }
2647
- async function handleStreamingRequest(res, request, targetProvider, targetModel, apiKey, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
2689
+ async function handleStreamingRequest(res, request, targetProvider, targetModel, apiKey, anthropicAuth, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
2648
2690
  let providerResponse;
2649
2691
  try {
2650
2692
  switch (targetProvider) {
2651
2693
  case "anthropic":
2652
- providerResponse = await forwardToAnthropicStream(request, targetModel, apiKey, betaHeaders);
2694
+ if (!anthropicAuth) throw new Error("No Anthropic auth");
2695
+ providerResponse = await forwardToAnthropicStream(request, targetModel, anthropicAuth, betaHeaders);
2653
2696
  break;
2654
2697
  case "google":
2655
2698
  providerResponse = await forwardToGeminiStream(request, targetModel, apiKey);
@@ -2727,13 +2770,14 @@ async function handleStreamingRequest(res, request, targetProvider, targetModel,
2727
2770
  });
2728
2771
  res.end();
2729
2772
  }
2730
- async function handleNonStreamingRequest(res, request, targetProvider, targetModel, apiKey, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
2773
+ async function handleNonStreamingRequest(res, request, targetProvider, targetModel, apiKey, anthropicAuth, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
2731
2774
  let providerResponse;
2732
2775
  let responseData;
2733
2776
  try {
2734
2777
  switch (targetProvider) {
2735
2778
  case "anthropic": {
2736
- providerResponse = await forwardToAnthropic(request, targetModel, apiKey, betaHeaders);
2779
+ if (!anthropicAuth) throw new Error("No Anthropic auth");
2780
+ providerResponse = await forwardToAnthropic(request, targetModel, anthropicAuth, betaHeaders);
2737
2781
  const rawData = await providerResponse.json();
2738
2782
  if (!providerResponse.ok) {
2739
2783
  res.writeHead(providerResponse.status, { "Content-Type": "application/json" });