@relayplane/proxy 1.5.22 → 1.5.24

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
@@ -48,6 +48,31 @@ A minimal config file:
48
48
 
49
49
  All configuration is optional — sensible defaults are applied for every field. The proxy merges your config with its defaults via deep merge, so you only need to specify what you want to change.
50
50
 
51
+ ## Architecture (Current)
52
+
53
+ ```text
54
+ Client (Claude Code / Aider / Cursor)
55
+ |
56
+ | OpenAI/Anthropic-compatible request
57
+ v
58
+ +-----------------------------------------------+
59
+ | RelayPlane Proxy (local) |
60
+ |-----------------------------------------------|
61
+ | 1) Parse request |
62
+ | 2) Infer task/complexity (pre-request) |
63
+ | 3) Select route/model |
64
+ | - explicit model / passthrough |
65
+ | - relayplane:auto/cost/fast/quality |
66
+ | - configured complexity/cascade rules |
67
+ | 4) Forward request to provider |
68
+ | 5) Return provider response |
69
+ | 6) (Optional) record telemetry metadata |
70
+ +-----------------------------------------------+
71
+ |
72
+ v
73
+ Provider APIs (Anthropic/OpenAI/Gemini/xAI/Moonshot/...)
74
+ ```
75
+
51
76
  ## How It Works
52
77
 
53
78
  RelayPlane is a local HTTP proxy. You point your agent at `localhost:4801` by setting `ANTHROPIC_BASE_URL` or `OPENAI_BASE_URL`. The proxy:
package/dist/cli.js CHANGED
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"standalone-proxy.d.ts","sourceRoot":"","sources":["../src/standalone-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAKlC,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,kBAAkB,CAAC;AAG3D,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAU5C,2DAA2D;AAC3D,eAAO,MAAM,mBAAmB,gBAAuB,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CA6C9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAa/E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAGrD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQ/E,CAAC;AAiCF;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAWjD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvD;AAkBD,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEjD,UAAU,WAAW;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;CAC9B;AAcD,UAAU,aAAa;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC;IAChD,cAAc,EAAE,MAAM,CAAC;CACxB;AAmBD,KAAK,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AA6EpD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,GAAG,KAAK,GAAG,MAAM,CAAC;CAChD;AAmZD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAe3D;AAuDD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,UAAU,CAiBrF;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,OAAO,CAIlG;AAq9CD;;GAEG;AACH,wBAAsB,UAAU,CAAC,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAkrC/E"}
1
+ {"version":3,"file":"standalone-proxy.d.ts","sourceRoot":"","sources":["../src/standalone-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAKlC,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,kBAAkB,CAAC;AAG3D,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAU5C,2DAA2D;AAC3D,eAAO,MAAM,mBAAmB,gBAAuB,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CA6C9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAa/E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAGrD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQ/E,CAAC;AAiCF;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAWjD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvD;AAkBD,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEjD,UAAU,WAAW;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;CAC9B;AAcD,UAAU,aAAa;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC;IAChD,cAAc,EAAE,MAAM,CAAC;CACxB;AAmBD,KAAK,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AA6EpD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,GAAG,KAAK,GAAG,MAAM,CAAC;CAChD;AAmZD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAe3D;AAuDD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,UAAU,CAiBrF;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,OAAO,CAIlG;AAu9CD;;GAEG;AACH,wBAAsB,UAAU,CAAC,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CA+sC/E"}
@@ -1823,7 +1823,7 @@ table{width:100%;border-collapse:collapse;font-size:.85rem}
1823
1823
  th{text-align:left;color:#64748b;font-weight:500;padding:8px 12px;border-bottom:1px solid #1e293b;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em}
1824
1824
  td{padding:8px 12px;border-bottom:1px solid #111318}
1825
1825
  .section{margin-bottom:32px}.section h2{font-size:1rem;font-weight:600;margin-bottom:12px;color:#94a3b8}
1826
- .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}.dot.up{background:#34d399}.dot.down{background:#ef4444}
1826
+ .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}.dot.up{background:#34d399}.dot.warn{background:#fbbf24}.dot.down{background:#ef4444}
1827
1827
  .badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:.75rem;font-weight:500}
1828
1828
  .badge.ok{background:#052e1633;color:#34d399}.badge.err{background:#2d0a0a;color:#ef4444}
1829
1829
  .badge.tt-code{background:#1e3a5f;color:#60a5fa}.badge.tt-analysis{background:#3b1f6e;color:#a78bfa}.badge.tt-summarization{background:#1a3a2a;color:#6ee7b7}.badge.tt-qa{background:#3a2f1e;color:#fbbf24}.badge.tt-general{background:#1e293b;color:#94a3b8}
@@ -1872,9 +1872,11 @@ async function load(){
1872
1872
  $('runs').innerHTML=(runsR.runs||[]).map(r=>
1873
1873
  '<tr><td>'+fmtTime(r.started_at)+'</td><td>'+r.model+'</td><td class="col-tt"><span class="badge '+ttCls(r.taskType)+'">'+(r.taskType||'general').replace(/_/g,' ')+'</span></td><td class="col-cx"><span class="badge '+cxCls(r.complexity)+'">'+(r.complexity||'simple')+'</span></td><td>'+(r.tokensIn||0)+'</td><td>'+(r.tokensOut||0)+'</td><td>$'+fmt(r.costUsd,4)+'</td><td>'+r.latencyMs+'ms</td><td><span class="badge '+(r.status==='success'?'ok':'err')+'">'+r.status+'</span></td></tr>'
1874
1874
  ).join('')||'<tr><td colspan=9 style="color:#64748b">No runs yet</td></tr>';
1875
- $('providers').innerHTML=(provH.providers||[]).map(p=>
1876
- '<div class="prov-item"><span class="dot '+(p.status==='healthy'?'up':'down')+'"></span>'+p.provider+'</div>'
1877
- ).join('');
1875
+ $('providers').innerHTML=(provH.providers||[]).map(p=>{
1876
+ const dotClass = p.status==='healthy'?'up':(p.status==='degraded'?'warn':'down');
1877
+ const rate = p.successRate!==undefined?(' '+Math.round(p.successRate*100)+'%'):'';
1878
+ return '<div class="prov-item"><span class="dot '+dotClass+'"></span>'+p.provider+rate+'</div>';
1879
+ }).join('');
1878
1880
  }catch(e){console.error(e)}
1879
1881
  }
1880
1882
  load();setInterval(load,5000);
@@ -2088,7 +2090,7 @@ async function startProxy(config = {}) {
2088
2090
  const offset = parseInt(params.get('offset') || '0', 10);
2089
2091
  const sorted = [...requestHistory].reverse();
2090
2092
  const runs = sorted.slice(offset, offset + limit).map(r => {
2091
- const origCost = (0, telemetry_js_1.estimateCost)(r.originalModel, r.tokensIn, r.tokensOut);
2093
+ const origCost = (0, telemetry_js_1.estimateCost)('claude-opus-4-20250514', r.tokensIn, r.tokensOut);
2092
2094
  const perRunSavings = Math.max(0, origCost - r.costUsd);
2093
2095
  return {
2094
2096
  id: r.id,
@@ -2117,15 +2119,15 @@ async function startProxy(config = {}) {
2117
2119
  return;
2118
2120
  }
2119
2121
  if (req.method === 'GET' && telemetryPath === 'savings') {
2120
- // Savings = cost(original requested model) - cost(actual routed-to model)
2121
- // If no routing happened (same model) → savings = 0
2122
- // Uses actual token counts from the response, so zeroed tokens produce zero savings.
2122
+ // Savings = cost if everything ran on Opus - actual cost
2123
+ // Always compare against Opus as the baseline
2124
+ const OPUS_BASELINE = 'claude-opus-4-20250514';
2123
2125
  let totalOriginalCost = 0;
2124
2126
  let totalActualCost = 0;
2125
2127
  let totalSavedAmount = 0;
2126
2128
  const byDayMap = new Map();
2127
2129
  for (const r of requestHistory) {
2128
- const origCost = (0, telemetry_js_1.estimateCost)(r.originalModel, r.tokensIn, r.tokensOut);
2130
+ const origCost = (0, telemetry_js_1.estimateCost)(OPUS_BASELINE, r.tokensIn, r.tokensOut);
2129
2131
  const actualCost = r.costUsd;
2130
2132
  const saved = Math.max(0, origCost - actualCost);
2131
2133
  totalOriginalCost += origCost;
@@ -2161,14 +2163,39 @@ async function startProxy(config = {}) {
2161
2163
  return;
2162
2164
  }
2163
2165
  if (req.method === 'GET' && telemetryPath === 'health') {
2166
+ // Calculate per-provider success rates from recent history (last 50 requests per provider)
2167
+ const providerStats = {};
2168
+ const recentHistory = requestHistory.slice(-200); // Look at last 200 requests
2169
+ for (const r of recentHistory) {
2170
+ const provider = r.provider || 'unknown';
2171
+ if (!providerStats[provider]) {
2172
+ providerStats[provider] = { success: 0, total: 0 };
2173
+ }
2174
+ providerStats[provider].total++;
2175
+ if (r.success) {
2176
+ providerStats[provider].success++;
2177
+ }
2178
+ }
2179
+ // Debug: log provider stats
2180
+ console.log('[RelayPlane Health] Provider stats:', JSON.stringify(providerStats));
2164
2181
  const providers = [];
2165
2182
  for (const [name, ep] of Object.entries(exports.DEFAULT_ENDPOINTS)) {
2166
2183
  const hasKey = !!process.env[ep.apiKeyEnv];
2184
+ const stats = providerStats[name.toLowerCase()];
2185
+ const successRate = stats && stats.total > 0 ? stats.success / stats.total : (hasKey ? 1 : 0);
2186
+ // Mark as unhealthy if success rate < 80% and has had requests
2187
+ let status = 'healthy';
2188
+ if (!hasKey) {
2189
+ status = 'down';
2190
+ }
2191
+ else if (stats && stats.total >= 5 && successRate < 0.8) {
2192
+ status = 'degraded';
2193
+ }
2167
2194
  providers.push({
2168
2195
  provider: name,
2169
- status: hasKey ? 'healthy' : 'down',
2196
+ status,
2170
2197
  latency: 0,
2171
- successRate: hasKey ? 1 : 0,
2198
+ successRate: Math.round(successRate * 100) / 100,
2172
2199
  lastChecked: new Date().toISOString(),
2173
2200
  });
2174
2201
  }