@relayplane/proxy 0.1.7 → 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 +122 -1
- package/dist/cli.js +184 -29
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +184 -29
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +119 -1
- package/dist/index.d.ts +119 -1
- package/dist/index.js +239 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +233 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1568,12 +1568,127 @@ ${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 AuthSchema = z.object({
|
|
1582
|
+
anthropicApiKey: z.string().optional(),
|
|
1583
|
+
anthropicMaxToken: z.string().optional(),
|
|
1584
|
+
useMaxForModels: z.array(z.string()).optional()
|
|
1585
|
+
// Default: ['opus']
|
|
1586
|
+
}).optional();
|
|
1587
|
+
var ConfigSchema = z.object({
|
|
1588
|
+
strategies: z.record(z.string(), StrategySchema).optional(),
|
|
1589
|
+
defaults: z.object({
|
|
1590
|
+
qualityModel: z.string().optional(),
|
|
1591
|
+
costModel: z.string().optional()
|
|
1592
|
+
}).optional(),
|
|
1593
|
+
auth: AuthSchema
|
|
1594
|
+
});
|
|
1595
|
+
var DEFAULT_CONFIG = {
|
|
1596
|
+
strategies: {
|
|
1597
|
+
code_review: { model: "anthropic:claude-sonnet-4-20250514" },
|
|
1598
|
+
code_generation: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1599
|
+
analysis: { model: "anthropic:claude-sonnet-4-20250514" },
|
|
1600
|
+
summarization: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1601
|
+
creative_writing: { model: "anthropic:claude-sonnet-4-20250514" },
|
|
1602
|
+
data_extraction: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1603
|
+
translation: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1604
|
+
question_answering: { model: "anthropic:claude-3-5-haiku-latest" },
|
|
1605
|
+
general: { model: "anthropic:claude-3-5-haiku-latest" }
|
|
1606
|
+
},
|
|
1607
|
+
defaults: {
|
|
1608
|
+
qualityModel: "claude-sonnet-4-20250514",
|
|
1609
|
+
costModel: "claude-3-5-haiku-latest"
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
function getConfigPath() {
|
|
1613
|
+
return path2.join(os2.homedir(), ".relayplane", "config.json");
|
|
1614
|
+
}
|
|
1615
|
+
function writeDefaultConfig() {
|
|
1616
|
+
const configPath = getConfigPath();
|
|
1617
|
+
const dir = path2.dirname(configPath);
|
|
1618
|
+
if (!fs2.existsSync(dir)) {
|
|
1619
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1620
|
+
}
|
|
1621
|
+
if (!fs2.existsSync(configPath)) {
|
|
1622
|
+
fs2.writeFileSync(
|
|
1623
|
+
configPath,
|
|
1624
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n",
|
|
1625
|
+
"utf-8"
|
|
1626
|
+
);
|
|
1627
|
+
console.log(`[relayplane] Created default config at ${configPath}`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
function loadConfig() {
|
|
1631
|
+
const configPath = getConfigPath();
|
|
1632
|
+
writeDefaultConfig();
|
|
1633
|
+
try {
|
|
1634
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
1635
|
+
const parsed = JSON.parse(raw);
|
|
1636
|
+
const validated = ConfigSchema.parse(parsed);
|
|
1637
|
+
return validated;
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
if (err instanceof z.ZodError) {
|
|
1640
|
+
console.error(`[relayplane] Invalid config: ${err.message}`);
|
|
1641
|
+
} else if (err instanceof SyntaxError) {
|
|
1642
|
+
console.error(`[relayplane] Config JSON parse error: ${err.message}`);
|
|
1643
|
+
} else {
|
|
1644
|
+
console.error(`[relayplane] Failed to load config: ${err}`);
|
|
1645
|
+
}
|
|
1646
|
+
console.log("[relayplane] Using default config");
|
|
1647
|
+
return DEFAULT_CONFIG;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function getStrategy(config, taskType) {
|
|
1651
|
+
return config.strategies?.[taskType] ?? null;
|
|
1652
|
+
}
|
|
1653
|
+
function getAnthropicAuth(config, model) {
|
|
1654
|
+
const auth = config.auth;
|
|
1655
|
+
const useMaxForModels = auth?.useMaxForModels ?? ["opus"];
|
|
1656
|
+
const shouldUseMax = useMaxForModels.some((m) => model.toLowerCase().includes(m.toLowerCase()));
|
|
1657
|
+
if (shouldUseMax && auth?.anthropicMaxToken) {
|
|
1658
|
+
return { type: "max", value: auth.anthropicMaxToken };
|
|
1659
|
+
}
|
|
1660
|
+
const apiKey = auth?.anthropicApiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
1661
|
+
if (apiKey) {
|
|
1662
|
+
return { type: "apiKey", value: apiKey };
|
|
1663
|
+
}
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
function watchConfig(onChange) {
|
|
1667
|
+
const configPath = getConfigPath();
|
|
1668
|
+
const dir = path2.dirname(configPath);
|
|
1669
|
+
if (!fs2.existsSync(dir)) {
|
|
1670
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1671
|
+
}
|
|
1672
|
+
let debounceTimer = null;
|
|
1673
|
+
fs2.watch(dir, (eventType, filename) => {
|
|
1674
|
+
if (filename === "config.json") {
|
|
1675
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1676
|
+
debounceTimer = setTimeout(() => {
|
|
1677
|
+
console.log("[relayplane] Config file changed, reloading...");
|
|
1678
|
+
const newConfig = loadConfig();
|
|
1679
|
+
onChange(newConfig);
|
|
1680
|
+
}, 100);
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1571
1685
|
// src/proxy.ts
|
|
1572
|
-
var VERSION = "0.1.
|
|
1686
|
+
var VERSION = "0.1.9";
|
|
1573
1687
|
var recentRuns = [];
|
|
1574
1688
|
var MAX_RECENT_RUNS = 100;
|
|
1575
1689
|
var modelCounts = {};
|
|
1576
1690
|
var serverStartTime = 0;
|
|
1691
|
+
var currentConfig = loadConfig();
|
|
1577
1692
|
var DEFAULT_ENDPOINTS = {
|
|
1578
1693
|
anthropic: {
|
|
1579
1694
|
baseUrl: "https://api.anthropic.com/v1",
|
|
@@ -1636,13 +1751,17 @@ function extractPromptText(messages) {
|
|
|
1636
1751
|
return "";
|
|
1637
1752
|
}).join("\n");
|
|
1638
1753
|
}
|
|
1639
|
-
async function forwardToAnthropic(request, targetModel,
|
|
1754
|
+
async function forwardToAnthropic(request, targetModel, auth, betaHeaders) {
|
|
1640
1755
|
const anthropicBody = buildAnthropicBody(request, targetModel, false);
|
|
1641
1756
|
const headers = {
|
|
1642
1757
|
"Content-Type": "application/json",
|
|
1643
|
-
"x-api-key": apiKey,
|
|
1644
1758
|
"anthropic-version": "2023-06-01"
|
|
1645
1759
|
};
|
|
1760
|
+
if (auth.type === "max") {
|
|
1761
|
+
headers["Authorization"] = `Bearer ${auth.value}`;
|
|
1762
|
+
} else {
|
|
1763
|
+
headers["x-api-key"] = auth.value;
|
|
1764
|
+
}
|
|
1646
1765
|
if (betaHeaders) {
|
|
1647
1766
|
headers["anthropic-beta"] = betaHeaders;
|
|
1648
1767
|
}
|
|
@@ -1653,13 +1772,17 @@ async function forwardToAnthropic(request, targetModel, apiKey, betaHeaders) {
|
|
|
1653
1772
|
});
|
|
1654
1773
|
return response;
|
|
1655
1774
|
}
|
|
1656
|
-
async function forwardToAnthropicStream(request, targetModel,
|
|
1775
|
+
async function forwardToAnthropicStream(request, targetModel, auth, betaHeaders) {
|
|
1657
1776
|
const anthropicBody = buildAnthropicBody(request, targetModel, true);
|
|
1658
1777
|
const headers = {
|
|
1659
1778
|
"Content-Type": "application/json",
|
|
1660
|
-
"x-api-key": apiKey,
|
|
1661
1779
|
"anthropic-version": "2023-06-01"
|
|
1662
1780
|
};
|
|
1781
|
+
if (auth.type === "max") {
|
|
1782
|
+
headers["Authorization"] = `Bearer ${auth.value}`;
|
|
1783
|
+
} else {
|
|
1784
|
+
headers["x-api-key"] = auth.value;
|
|
1785
|
+
}
|
|
1663
1786
|
if (betaHeaders) {
|
|
1664
1787
|
headers["anthropic-beta"] = betaHeaders;
|
|
1665
1788
|
}
|
|
@@ -2424,42 +2547,65 @@ async function startProxy(config = {}) {
|
|
|
2424
2547
|
const confidence = getInferenceConfidence(promptText, taskType);
|
|
2425
2548
|
log(`Inferred task: ${taskType} (confidence: ${confidence.toFixed(2)})`);
|
|
2426
2549
|
if (routingMode !== "passthrough") {
|
|
2427
|
-
const
|
|
2428
|
-
if (
|
|
2429
|
-
const parsed = parsePreferredModel(
|
|
2550
|
+
const configStrategy = getStrategy(currentConfig, taskType);
|
|
2551
|
+
if (configStrategy) {
|
|
2552
|
+
const parsed = parsePreferredModel(configStrategy.model);
|
|
2430
2553
|
if (parsed) {
|
|
2431
2554
|
targetProvider = parsed.provider;
|
|
2432
2555
|
targetModel = parsed.model;
|
|
2433
|
-
log(`Using
|
|
2556
|
+
log(`Using config strategy: ${configStrategy.model}`);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (!configStrategy) {
|
|
2560
|
+
const rule = relay.routing.get(taskType);
|
|
2561
|
+
if (rule && rule.preferredModel) {
|
|
2562
|
+
const parsed = parsePreferredModel(rule.preferredModel);
|
|
2563
|
+
if (parsed) {
|
|
2564
|
+
targetProvider = parsed.provider;
|
|
2565
|
+
targetModel = parsed.model;
|
|
2566
|
+
log(`Using learned rule: ${rule.preferredModel}`);
|
|
2567
|
+
} else {
|
|
2568
|
+
const defaultRoute = DEFAULT_ROUTING[taskType];
|
|
2569
|
+
targetProvider = defaultRoute.provider;
|
|
2570
|
+
targetModel = defaultRoute.model;
|
|
2571
|
+
}
|
|
2434
2572
|
} else {
|
|
2435
2573
|
const defaultRoute = DEFAULT_ROUTING[taskType];
|
|
2436
2574
|
targetProvider = defaultRoute.provider;
|
|
2437
2575
|
targetModel = defaultRoute.model;
|
|
2438
2576
|
}
|
|
2439
|
-
} else {
|
|
2440
|
-
const defaultRoute = DEFAULT_ROUTING[taskType];
|
|
2441
|
-
targetProvider = defaultRoute.provider;
|
|
2442
|
-
targetModel = defaultRoute.model;
|
|
2443
2577
|
}
|
|
2444
2578
|
if (routingMode === "cost") {
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2579
|
+
const costModel = currentConfig.defaults?.costModel || "claude-3-5-haiku-latest";
|
|
2580
|
+
targetModel = costModel;
|
|
2581
|
+
targetProvider = "anthropic";
|
|
2582
|
+
log(`Cost mode: using ${costModel}`);
|
|
2450
2583
|
} else if (routingMode === "quality") {
|
|
2451
|
-
const qualityModel = process.env["RELAYPLANE_QUALITY_MODEL"] || "claude-sonnet-4-20250514";
|
|
2584
|
+
const qualityModel = currentConfig.defaults?.qualityModel || process.env["RELAYPLANE_QUALITY_MODEL"] || "claude-sonnet-4-20250514";
|
|
2452
2585
|
targetModel = qualityModel;
|
|
2453
2586
|
targetProvider = "anthropic";
|
|
2587
|
+
log(`Quality mode: using ${qualityModel}`);
|
|
2454
2588
|
}
|
|
2455
2589
|
}
|
|
2456
2590
|
log(`Routing to: ${targetProvider}/${targetModel}`);
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
if (
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2591
|
+
let apiKey;
|
|
2592
|
+
let anthropicAuth = null;
|
|
2593
|
+
if (targetProvider === "anthropic") {
|
|
2594
|
+
anthropicAuth = getAnthropicAuth(currentConfig, targetModel);
|
|
2595
|
+
if (!anthropicAuth) {
|
|
2596
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2597
|
+
res.end(JSON.stringify({ error: "No Anthropic auth configured (set ANTHROPIC_API_KEY or config.auth.anthropicMaxToken)" }));
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
log(`Using ${anthropicAuth.type === "max" ? "MAX token" : "API key"} auth for ${targetModel}`);
|
|
2601
|
+
} else {
|
|
2602
|
+
const apiKeyEnv = DEFAULT_ENDPOINTS[targetProvider]?.apiKeyEnv ?? `${targetProvider.toUpperCase()}_API_KEY`;
|
|
2603
|
+
apiKey = process.env[apiKeyEnv];
|
|
2604
|
+
if (!apiKey) {
|
|
2605
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2606
|
+
res.end(JSON.stringify({ error: `Missing ${apiKeyEnv} environment variable` }));
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2463
2609
|
}
|
|
2464
2610
|
const startTime = Date.now();
|
|
2465
2611
|
const betaHeaders = req.headers["anthropic-beta"];
|
|
@@ -2470,6 +2616,7 @@ async function startProxy(config = {}) {
|
|
|
2470
2616
|
targetProvider,
|
|
2471
2617
|
targetModel,
|
|
2472
2618
|
apiKey,
|
|
2619
|
+
anthropicAuth,
|
|
2473
2620
|
relay,
|
|
2474
2621
|
promptText,
|
|
2475
2622
|
taskType,
|
|
@@ -2486,6 +2633,7 @@ async function startProxy(config = {}) {
|
|
|
2486
2633
|
targetProvider,
|
|
2487
2634
|
targetModel,
|
|
2488
2635
|
apiKey,
|
|
2636
|
+
anthropicAuth,
|
|
2489
2637
|
relay,
|
|
2490
2638
|
promptText,
|
|
2491
2639
|
taskType,
|
|
@@ -2497,6 +2645,10 @@ async function startProxy(config = {}) {
|
|
|
2497
2645
|
);
|
|
2498
2646
|
}
|
|
2499
2647
|
});
|
|
2648
|
+
watchConfig((newConfig) => {
|
|
2649
|
+
currentConfig = newConfig;
|
|
2650
|
+
console.log("[relayplane] Config reloaded");
|
|
2651
|
+
});
|
|
2500
2652
|
return new Promise((resolve, reject) => {
|
|
2501
2653
|
server.on("error", reject);
|
|
2502
2654
|
server.listen(port, host, () => {
|
|
@@ -2505,17 +2657,19 @@ async function startProxy(config = {}) {
|
|
|
2505
2657
|
console.log(` Models: relayplane:auto, relayplane:cost, relayplane:quality`);
|
|
2506
2658
|
console.log(` Endpoint: POST /v1/chat/completions`);
|
|
2507
2659
|
console.log(` Stats: GET /stats, /runs, /health`);
|
|
2660
|
+
console.log(` Config: ~/.relayplane/config.json (hot-reload enabled)`);
|
|
2508
2661
|
console.log(` Streaming: \u2705 Enabled`);
|
|
2509
2662
|
resolve(server);
|
|
2510
2663
|
});
|
|
2511
2664
|
});
|
|
2512
2665
|
}
|
|
2513
|
-
async function handleStreamingRequest(res, request, targetProvider, targetModel, apiKey, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
|
|
2666
|
+
async function handleStreamingRequest(res, request, targetProvider, targetModel, apiKey, anthropicAuth, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
|
|
2514
2667
|
let providerResponse;
|
|
2515
2668
|
try {
|
|
2516
2669
|
switch (targetProvider) {
|
|
2517
2670
|
case "anthropic":
|
|
2518
|
-
|
|
2671
|
+
if (!anthropicAuth) throw new Error("No Anthropic auth");
|
|
2672
|
+
providerResponse = await forwardToAnthropicStream(request, targetModel, anthropicAuth, betaHeaders);
|
|
2519
2673
|
break;
|
|
2520
2674
|
case "google":
|
|
2521
2675
|
providerResponse = await forwardToGeminiStream(request, targetModel, apiKey);
|
|
@@ -2593,13 +2747,14 @@ async function handleStreamingRequest(res, request, targetProvider, targetModel,
|
|
|
2593
2747
|
});
|
|
2594
2748
|
res.end();
|
|
2595
2749
|
}
|
|
2596
|
-
async function handleNonStreamingRequest(res, request, targetProvider, targetModel, apiKey, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
|
|
2750
|
+
async function handleNonStreamingRequest(res, request, targetProvider, targetModel, apiKey, anthropicAuth, relay, promptText, taskType, confidence, routingMode, startTime, log, betaHeaders) {
|
|
2597
2751
|
let providerResponse;
|
|
2598
2752
|
let responseData;
|
|
2599
2753
|
try {
|
|
2600
2754
|
switch (targetProvider) {
|
|
2601
2755
|
case "anthropic": {
|
|
2602
|
-
|
|
2756
|
+
if (!anthropicAuth) throw new Error("No Anthropic auth");
|
|
2757
|
+
providerResponse = await forwardToAnthropic(request, targetModel, anthropicAuth, betaHeaders);
|
|
2603
2758
|
const rawData = await providerResponse.json();
|
|
2604
2759
|
if (!providerResponse.ok) {
|
|
2605
2760
|
res.writeHead(providerResponse.status, { "Content-Type": "application/json" });
|