@relayplane/proxy 1.8.23 → 1.8.25

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.
@@ -1 +1 @@
1
- {"version":3,"file":"standalone-proxy.d.ts","sourceRoot":"","sources":["../src/standalone-proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"standalone-proxy.d.ts","sourceRoot":"","sources":["../src/standalone-proxy.ts"],"names":[],"mappings":"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"}
@@ -219,6 +219,41 @@ exports.MODEL_MAPPING = {
219
219
  'gpt-4o': { provider: 'openai', model: 'gpt-4o' },
220
220
  'gpt-4o-mini': { provider: 'openai', model: 'gpt-4o-mini' },
221
221
  'gpt-4.1': { provider: 'openai', model: 'gpt-4.1' },
222
+ // OpenAI GPT-5 family
223
+ 'gpt-5.4': { provider: 'openai', model: 'gpt-5.4' },
224
+ 'gpt-5.4-pro': { provider: 'openai', model: 'gpt-5.4-pro' },
225
+ 'gpt-5.3': { provider: 'openai', model: 'gpt-5.3-chat' },
226
+ 'gpt-5.2': { provider: 'openai', model: 'gpt-5.2' },
227
+ 'gpt-5.1': { provider: 'openai', model: 'gpt-5.1' },
228
+ 'gpt-5': { provider: 'openai', model: 'gpt-5.4' },
229
+ 'gpt-5-mini': { provider: 'openai', model: 'gpt-5-mini' },
230
+ 'gpt-5-nano': { provider: 'openai', model: 'gpt-5-nano' },
231
+ // OpenAI GPT-4.1
232
+ 'gpt-4.1-mini': { provider: 'openai', model: 'gpt-4.1-mini' },
233
+ 'gpt-4.1-nano': { provider: 'openai', model: 'gpt-4.1-nano' },
234
+ // OpenAI O-series reasoning
235
+ 'o3': { provider: 'openai', model: 'o3' },
236
+ 'o3-pro': { provider: 'openai', model: 'o3-pro' },
237
+ 'o3-mini': { provider: 'openai', model: 'o3-mini' },
238
+ 'o4-mini': { provider: 'openai', model: 'o4-mini' },
239
+ // Google Gemini
240
+ 'gemini-3.1-pro': { provider: 'google', model: 'gemini-3.1-pro-preview' },
241
+ 'gemini-3-pro': { provider: 'google', model: 'gemini-3-pro-preview' },
242
+ 'gemini-3-flash': { provider: 'google', model: 'gemini-3-flash-preview' },
243
+ 'gemini-2.5-pro': { provider: 'google', model: 'gemini-2.5-pro' },
244
+ 'gemini-2.5-flash': { provider: 'google', model: 'gemini-2.5-flash' },
245
+ 'gemini-2.5-flash-lite': { provider: 'google', model: 'gemini-2.5-flash-lite' },
246
+ 'gemini-2.0-flash': { provider: 'google', model: 'gemini-2.0-flash' },
247
+ // xAI Grok
248
+ 'grok-4.20': { provider: 'xai', model: 'grok-4.20-beta' },
249
+ 'grok-4': { provider: 'xai', model: 'grok-4' },
250
+ 'grok-4-fast': { provider: 'xai', model: 'grok-4-fast' },
251
+ 'grok-4.1-fast': { provider: 'xai', model: 'grok-4.1-fast' },
252
+ 'grok-3': { provider: 'xai', model: 'grok-3' },
253
+ 'grok-3-mini': { provider: 'xai', model: 'grok-3-mini' },
254
+ // DeepSeek
255
+ 'deepseek': { provider: 'deepseek', model: 'deepseek-chat' },
256
+ 'deepseek-r1': { provider: 'deepseek', model: 'deepseek-reasoner' },
222
257
  };
223
258
  /**
224
259
  * RelayPlane model aliases - resolve before routing
@@ -236,9 +271,9 @@ exports.RELAYPLANE_ALIASES = {
236
271
  */
237
272
  exports.SMART_ALIASES = {
238
273
  // Defaults: OpenRouter (used when no env vars are available)
239
- 'rp:best': { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-5' },
274
+ 'rp:best': { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-6' },
240
275
  'rp:fast': { provider: 'openrouter', model: 'anthropic/claude-3-5-haiku' },
241
- 'rp:cheap': { provider: 'openrouter', model: 'google/gemini-2.0-flash-001' },
276
+ 'rp:cheap': { provider: 'openrouter', model: 'google/gemini-2.5-flash-lite' },
242
277
  'rp:balanced': { provider: 'openrouter', model: 'anthropic/claude-3-5-haiku' },
243
278
  };
244
279
  /**
@@ -251,9 +286,9 @@ function buildSmartAliases() {
251
286
  return {
252
287
  via: 'openrouter',
253
288
  aliases: {
254
- 'rp:best': { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-5' },
289
+ 'rp:best': { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-6' },
255
290
  'rp:fast': { provider: 'openrouter', model: 'anthropic/claude-3-5-haiku' },
256
- 'rp:cheap': { provider: 'openrouter', model: 'google/gemini-2.0-flash-001' },
291
+ 'rp:cheap': { provider: 'openrouter', model: 'google/gemini-2.5-flash-lite' },
257
292
  'rp:balanced': { provider: 'openrouter', model: 'anthropic/claude-3-5-haiku' },
258
293
  },
259
294
  };
@@ -284,9 +319,9 @@ function buildSmartAliases() {
284
319
  return {
285
320
  via: 'openrouter (fallback — no API keys detected)',
286
321
  aliases: {
287
- 'rp:best': { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-5' },
322
+ 'rp:best': { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-6' },
288
323
  'rp:fast': { provider: 'openrouter', model: 'anthropic/claude-3-5-haiku' },
289
- 'rp:cheap': { provider: 'openrouter', model: 'google/gemini-2.0-flash-001' },
324
+ 'rp:cheap': { provider: 'openrouter', model: 'google/gemini-2.5-flash-lite' },
290
325
  'rp:balanced': { provider: 'openrouter', model: 'anthropic/claude-3-5-haiku' },
291
326
  },
292
327
  };
@@ -365,10 +400,10 @@ function resolveModelAlias(model) {
365
400
  return model;
366
401
  }
367
402
  /**
368
- * Default routing based on task type
369
- * Uses Haiku 3.5 for cost optimization, upgrades based on learned rules
403
+ * Default routing based on task type.
404
+ * Updated at proxy startup by provider auto-detection via detectAvailableProviders().
370
405
  */
371
- const DEFAULT_ROUTING = {
406
+ let DEFAULT_ROUTING = {
372
407
  code_generation: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
373
408
  code_review: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
374
409
  summarization: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
@@ -379,6 +414,113 @@ const DEFAULT_ROUTING = {
379
414
  question_answering: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
380
415
  general: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
381
416
  };
417
+ /**
418
+ * Parse a complexity routing config value into a provider/model pair.
419
+ * Accepts:
420
+ * - plain model name string: "claude-sonnet-4-6"
421
+ * - provider/model slash notation: "google/gemini-2.5-flash-lite"
422
+ * - openrouter prefix: "openrouter/anthropic/claude-sonnet-4-6"
423
+ * - object: { provider: "google", model: "gemini-2.5-flash-lite" }
424
+ */
425
+ function parseComplexityModel(val) {
426
+ if (typeof val === 'object' && val !== null) {
427
+ return val;
428
+ }
429
+ if (typeof val === 'string') {
430
+ if (val.includes('/')) {
431
+ const idx = val.indexOf('/');
432
+ const provider = val.slice(0, idx);
433
+ const model = val.slice(idx + 1); // preserves openrouter/anthropic/claude-... style
434
+ return { provider, model };
435
+ }
436
+ // Plain model name — look up in MODEL_MAPPING, fallback to anthropic
437
+ return exports.MODEL_MAPPING[val] ?? { provider: 'anthropic', model: val };
438
+ }
439
+ return { provider: 'anthropic', model: 'claude-sonnet-4-6' };
440
+ }
441
+ /** Per-provider default complexity tier models */
442
+ const PROVIDER_COMPLEXITY_TIERS = {
443
+ anthropic: {
444
+ simple: { provider: 'anthropic', model: 'claude-haiku-4-5' },
445
+ moderate: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
446
+ complex: { provider: 'anthropic', model: 'claude-opus-4-6' },
447
+ },
448
+ openai: {
449
+ simple: { provider: 'openai', model: 'gpt-4.1-mini' },
450
+ moderate: { provider: 'openai', model: 'gpt-5.4' },
451
+ complex: { provider: 'openai', model: 'gpt-5.4' },
452
+ },
453
+ google: {
454
+ simple: { provider: 'google', model: 'gemini-2.5-flash-lite' },
455
+ moderate: { provider: 'google', model: 'gemini-2.5-flash' },
456
+ complex: { provider: 'google', model: 'gemini-2.5-pro' },
457
+ },
458
+ xai: {
459
+ simple: { provider: 'xai', model: 'grok-4.1-fast' },
460
+ moderate: { provider: 'xai', model: 'grok-4.20-beta' },
461
+ complex: { provider: 'xai', model: 'grok-4' },
462
+ },
463
+ deepseek: {
464
+ simple: { provider: 'deepseek', model: 'deepseek-chat' },
465
+ moderate: { provider: 'deepseek', model: 'deepseek-chat' },
466
+ complex: { provider: 'deepseek', model: 'deepseek-reasoner' },
467
+ },
468
+ openrouter: {
469
+ simple: { provider: 'openrouter', model: 'google/gemini-2.5-flash-lite' },
470
+ moderate: { provider: 'openrouter', model: 'google/gemini-2.5-flash' },
471
+ complex: { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-6' },
472
+ },
473
+ };
474
+ /**
475
+ * Detect which AI providers are available based on env vars and user config.
476
+ * Returns providers in priority order: anthropic > openai > google > xai > deepseek > openrouter > groq
477
+ */
478
+ function detectAvailableProviders(userConfig) {
479
+ const cfg = (userConfig ?? {});
480
+ const auth = (cfg['auth'] ?? {});
481
+ const available = [];
482
+ if (process.env['ANTHROPIC_API_KEY'] || auth['anthropicApiKey'] || auth['anthropicMaxToken']) {
483
+ available.push('anthropic');
484
+ }
485
+ if (process.env['OPENAI_API_KEY'] || auth['openaiApiKey']) {
486
+ available.push('openai');
487
+ }
488
+ if (process.env['GOOGLE_API_KEY'] || process.env['GEMINI_API_KEY'] || auth['googleApiKey']) {
489
+ available.push('google');
490
+ }
491
+ if (process.env['XAI_API_KEY'] || auth['xaiApiKey']) {
492
+ available.push('xai');
493
+ }
494
+ if (process.env['DEEPSEEK_API_KEY'] || auth['deepseekApiKey']) {
495
+ available.push('deepseek');
496
+ }
497
+ if (process.env['OPENROUTER_API_KEY'] || auth['openrouterApiKey']) {
498
+ available.push('openrouter');
499
+ }
500
+ if (process.env['GROQ_API_KEY'] || auth['groqApiKey']) {
501
+ available.push('groq');
502
+ }
503
+ return available;
504
+ }
505
+ /**
506
+ * Build default complexity tiers based on first detected provider.
507
+ * Config overrides win — only fills in tiers not explicitly set.
508
+ */
509
+ function buildDefaultComplexityTiers(providers, existing) {
510
+ // Find first provider that has a known tier mapping
511
+ const primaryProvider = providers.find((p) => PROVIDER_COMPLEXITY_TIERS[p]) ?? 'anthropic';
512
+ const defaults = PROVIDER_COMPLEXITY_TIERS[primaryProvider] ?? PROVIDER_COMPLEXITY_TIERS['anthropic'];
513
+ const simple = existing?.simple != null
514
+ ? parseComplexityModel(existing.simple)
515
+ : defaults.simple;
516
+ const moderate = existing?.moderate != null
517
+ ? parseComplexityModel(existing.moderate)
518
+ : defaults.moderate;
519
+ const complex = existing?.complex != null
520
+ ? parseComplexityModel(existing.complex)
521
+ : defaults.complex;
522
+ return { simple, moderate, complex };
523
+ }
382
524
  const UNCERTAINTY_PATTERNS = [
383
525
  /i'?m not (entirely |completely |really )?sure/i,
384
526
  /i don'?t (really |actually )?know/i,
@@ -1411,6 +1553,38 @@ function convertMessagesToGemini(messages) {
1411
1553
  }
1412
1554
  return geminiContents;
1413
1555
  }
1556
+ /**
1557
+ * Recursively strip JSON Schema properties that Gemini rejects but OpenAI/Anthropic accept.
1558
+ * Gemini rejects: patternProperties, additionalProperties (boolean), $schema, definitions, $defs, unevaluatedProperties
1559
+ */
1560
+ function sanitizeSchemaForGemini(schema) {
1561
+ if (Array.isArray(schema)) {
1562
+ return schema.map(sanitizeSchemaForGemini);
1563
+ }
1564
+ if (schema !== null && typeof schema === 'object') {
1565
+ const obj = schema;
1566
+ const result = {};
1567
+ for (const [key, value] of Object.entries(obj)) {
1568
+ // Strip fields Gemini doesn't support
1569
+ if (key === 'patternProperties')
1570
+ continue;
1571
+ if (key === '$schema')
1572
+ continue;
1573
+ if (key === 'definitions')
1574
+ continue;
1575
+ if (key === '$defs')
1576
+ continue;
1577
+ if (key === 'unevaluatedProperties')
1578
+ continue;
1579
+ // additionalProperties: Gemini only accepts object form, not boolean
1580
+ if (key === 'additionalProperties' && typeof value === 'boolean')
1581
+ continue;
1582
+ result[key] = sanitizeSchemaForGemini(value);
1583
+ }
1584
+ return result;
1585
+ }
1586
+ return schema;
1587
+ }
1414
1588
  /**
1415
1589
  * Forward non-streaming request to Gemini API
1416
1590
  */
@@ -1436,7 +1610,7 @@ async function forwardToGemini(request, targetModel, apiKey) {
1436
1610
  functionDeclarations: request.tools.map((t) => ({
1437
1611
  name: t.function.name,
1438
1612
  description: t.function.description || "",
1439
- parameters: t.function.parameters || {}
1613
+ parameters: sanitizeSchemaForGemini(t.function.parameters || {})
1440
1614
  }))
1441
1615
  }];
1442
1616
  }
@@ -1474,7 +1648,7 @@ async function forwardToGeminiStream(request, targetModel, apiKey) {
1474
1648
  functionDeclarations: request.tools.map((t) => ({
1475
1649
  name: t.function.name,
1476
1650
  description: t.function.description || "",
1477
- parameters: t.function.parameters || {}
1651
+ parameters: sanitizeSchemaForGemini(t.function.parameters || {})
1478
1652
  }))
1479
1653
  }];
1480
1654
  }
@@ -2235,18 +2409,25 @@ function getCooldownConfig(config) {
2235
2409
  };
2236
2410
  return { ...defaults, ...config.reliability?.cooldowns };
2237
2411
  }
2412
+ function complexityValToString(val) {
2413
+ if (val == null)
2414
+ return undefined;
2415
+ if (typeof val === 'string')
2416
+ return val;
2417
+ return `${val.provider}/${val.model}`;
2418
+ }
2238
2419
  function getCostModel(config) {
2239
- return (config.routing?.complexity?.simple ||
2420
+ return (complexityValToString(config.routing?.complexity?.simple) ||
2240
2421
  config.routing?.cascade?.models?.[0] ||
2241
2422
  'claude-haiku-4-5');
2242
2423
  }
2243
2424
  function getFastModel(config) {
2244
- return (config.routing?.complexity?.simple ||
2425
+ return (complexityValToString(config.routing?.complexity?.simple) ||
2245
2426
  config.routing?.cascade?.models?.[0] ||
2246
2427
  'claude-haiku-4-5');
2247
2428
  }
2248
2429
  function getQualityModel(config) {
2249
- return (config.routing?.complexity?.complex ||
2430
+ return (complexityValToString(config.routing?.complexity?.complex) ||
2250
2431
  config.routing?.cascade?.models?.[config.routing?.cascade?.models?.length ? config.routing.cascade.models.length - 1 : 0] ||
2251
2432
  process.env['RELAYPLANE_QUALITY_MODEL'] ||
2252
2433
  'claude-sonnet-4-6');
@@ -2604,61 +2785,72 @@ async function startProxy(config = {}) {
2604
2785
  log(`[CROSS-CASCADE] Enabled. Provider order: ${proxyConfig.crossProviderCascade.providers.join(' → ')}`);
2605
2786
  }
2606
2787
  const isFirstRun = !rawFileHasRouting || !userConfig.first_run_complete;
2788
+ // Always detect available providers and update DEFAULT_ROUTING at startup
2789
+ const availableProviders = detectAvailableProviders();
2790
+ {
2791
+ // Build human-readable provider labels for startup log
2792
+ const providerLabels = availableProviders.map((p) => {
2793
+ if (p === 'anthropic') {
2794
+ const key = process.env['ANTHROPIC_API_KEY'] || '';
2795
+ return key.startsWith('sk-ant-api') ? '✓ Anthropic' : '✓ Anthropic (Max)';
2796
+ }
2797
+ return `✓ ${p.charAt(0).toUpperCase() + p.slice(1)}`;
2798
+ });
2799
+ if (providerLabels.length > 0) {
2800
+ console.log(`[RelayPlane] ${providerLabels.join(', ')}`);
2801
+ }
2802
+ // Build default tiers, respecting any existing user config overrides
2803
+ const existingComplexity = proxyConfig.routing?.complexity;
2804
+ const defaultProviders = availableProviders.length > 0 ? availableProviders : ['openrouter'];
2805
+ const tiers = buildDefaultComplexityTiers(defaultProviders, existingComplexity);
2806
+ // Update DEFAULT_ROUTING with detected provider's moderate tier
2807
+ const moderateRoute = tiers.moderate;
2808
+ const allTaskTypes = ['code_generation', 'code_review', 'summarization', 'analysis', 'creative_writing', 'data_extraction', 'translation', 'question_answering', 'general'];
2809
+ for (const tt of allTaskTypes) {
2810
+ DEFAULT_ROUTING[tt] = moderateRoute;
2811
+ }
2812
+ console.log(`[RelayPlane] Auto-routing: simple=${tiers.simple.model}, moderate=${tiers.moderate.model}, complex=${tiers.complex.model}`);
2813
+ }
2607
2814
  if (isFirstRun || proxyConfig.routing?.mode === 'auto') {
2608
2815
  const envAnthropicKey = process.env['ANTHROPIC_API_KEY'];
2609
2816
  const hasRegularApiKey = !!envAnthropicKey && envAnthropicKey.startsWith('sk-ant-api');
2610
- if (hasRegularApiKey) {
2611
- // Full 3-tier routing with API key
2612
- console.log('[RelayPlane] Auto-config: ANTHROPIC_API_KEY detected — enabling 3-tier routing (haiku/sonnet/opus)');
2613
- if (isFirstRun) {
2614
- // Merge routing into existing config, preserving user fields (device_id, etc.)
2615
- let existingRaw = {};
2616
- try {
2617
- existingRaw = JSON.parse(await fs.promises.readFile(configPath, 'utf8'));
2618
- }
2619
- catch { /* fresh start, no existing config */ }
2620
- const autoRouting = {
2621
- mode: 'complexity',
2622
- cascade: { enabled: false, models: [], escalateOn: 'uncertainty', maxEscalations: 1 },
2623
- complexity: {
2624
- enabled: true,
2625
- simple: 'claude-3-5-haiku-latest',
2626
- moderate: 'claude-sonnet-4-6',
2627
- complex: 'claude-opus-4-6',
2628
- },
2817
+ if (isFirstRun) {
2818
+ let existingRaw = {};
2819
+ try {
2820
+ existingRaw = JSON.parse(await fs.promises.readFile(configPath, 'utf8'));
2821
+ }
2822
+ catch { /* fresh start, no existing config */ }
2823
+ let autoComplexity;
2824
+ if (availableProviders.includes('anthropic') && hasRegularApiKey) {
2825
+ // Full Anthropic API key — enable haiku 3-tier routing
2826
+ console.log('[RelayPlane] Auto-config: ANTHROPIC_API_KEY detected enabling 3-tier routing (haiku/sonnet/opus)');
2827
+ autoComplexity = { simple: 'claude-haiku-4-5', moderate: 'claude-sonnet-4-6', complex: 'claude-opus-4-6' };
2828
+ }
2829
+ else if (availableProviders.length > 0 && !availableProviders.includes('anthropic')) {
2830
+ // Non-Anthropic provider — use detected provider's tiers
2831
+ const providerTiers = buildDefaultComplexityTiers(availableProviders);
2832
+ console.log(`[RelayPlane] Auto-config: ${availableProviders[0]} detected — enabling provider-aware 3-tier routing`);
2833
+ autoComplexity = {
2834
+ simple: `${providerTiers.simple.provider}/${providerTiers.simple.model}`,
2835
+ moderate: `${providerTiers.moderate.provider}/${providerTiers.moderate.model}`,
2836
+ complex: `${providerTiers.complex.provider}/${providerTiers.complex.model}`,
2629
2837
  };
2630
- const updatedConfig = { ...existingRaw, routing: autoRouting, first_run_complete: true };
2631
- await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
2632
- await fs.promises.writeFile(configPath, JSON.stringify(updatedConfig, null, 2), 'utf8');
2633
- proxyConfig = await loadProxyConfig(configPath, log);
2634
- console.log(`[RelayPlane] Auto-config: wrote 3-tier routing config to ${configPath}`);
2635
2838
  }
2636
- }
2637
- else {
2638
- // No regular API key — OAuth only or no Anthropic key; skip Haiku (OAuth not supported for Haiku)
2639
- if (isFirstRun) {
2839
+ else {
2840
+ // OAuth only or no API key — skip Haiku (OAuth not supported for Haiku)
2640
2841
  console.warn('[RelayPlane] ⚠️ No ANTHROPIC_API_KEY (sk-ant-api*) — Haiku disabled. Set ANTHROPIC_API_KEY to enable 3-tier routing.');
2641
- let existingRaw = {};
2642
- try {
2643
- existingRaw = JSON.parse(await fs.promises.readFile(configPath, 'utf8'));
2644
- }
2645
- catch { /* fresh start, no existing config */ }
2646
- const autoRouting = {
2647
- mode: 'complexity',
2648
- cascade: { enabled: false, models: [], escalateOn: 'uncertainty', maxEscalations: 1 },
2649
- complexity: {
2650
- enabled: true,
2651
- simple: 'claude-sonnet-4-6',
2652
- moderate: 'claude-sonnet-4-6',
2653
- complex: 'claude-opus-4-6',
2654
- },
2655
- };
2656
- const updatedConfig = { ...existingRaw, routing: autoRouting, first_run_complete: true };
2657
- await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
2658
- await fs.promises.writeFile(configPath, JSON.stringify(updatedConfig, null, 2), 'utf8');
2659
- proxyConfig = await loadProxyConfig(configPath, log);
2660
- console.log(`[RelayPlane] Auto-config: wrote OAuth-safe config to ${configPath} (no Haiku)`);
2842
+ autoComplexity = { simple: 'claude-sonnet-4-6', moderate: 'claude-sonnet-4-6', complex: 'claude-opus-4-6' };
2661
2843
  }
2844
+ const autoRouting = {
2845
+ mode: 'complexity',
2846
+ cascade: { enabled: false, models: [], escalateOn: 'uncertainty', maxEscalations: 1 },
2847
+ complexity: { enabled: true, ...autoComplexity },
2848
+ };
2849
+ const updatedConfig = { ...existingRaw, routing: autoRouting, first_run_complete: true };
2850
+ await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
2851
+ await fs.promises.writeFile(configPath, JSON.stringify(updatedConfig, null, 2), 'utf8');
2852
+ proxyConfig = await loadProxyConfig(configPath, log);
2853
+ console.log(`[RelayPlane] Auto-config: wrote routing config to ${configPath}`);
2662
2854
  }
2663
2855
  }
2664
2856
  }
@@ -3193,8 +3385,6 @@ async function startProxy(config = {}) {
3193
3385
  providerStats[provider].success++;
3194
3386
  }
3195
3387
  }
3196
- // Debug: log provider stats
3197
- console.log('[RelayPlane Health] Provider stats:', JSON.stringify(providerStats));
3198
3388
  const providers = [];
3199
3389
  for (const [name, ep] of Object.entries(exports.DEFAULT_ENDPOINTS)) {
3200
3390
  // Skip Ollama from normal key-based health check — it's handled separately
@@ -3558,7 +3748,11 @@ async function startProxy(config = {}) {
3558
3748
  useCascade = false; // Disable full cascade, use complexity routing instead
3559
3749
  let selectedModel = null;
3560
3750
  if (proxyConfig.routing?.complexity?.enabled) {
3561
- selectedModel = proxyConfig.routing?.complexity?.[complexity];
3751
+ const complexityVal = proxyConfig.routing?.complexity?.[complexity];
3752
+ if (complexityVal != null) {
3753
+ const parsed = parseComplexityModel(complexityVal);
3754
+ selectedModel = `${parsed.provider}/${parsed.model}`;
3755
+ }
3562
3756
  }
3563
3757
  else {
3564
3758
  selectedModel = getCascadeModels(proxyConfig)[0] || getCostModel(proxyConfig);
@@ -3600,9 +3794,12 @@ async function startProxy(config = {}) {
3600
3794
  else {
3601
3795
  // Complexity-based routing takes priority when enabled
3602
3796
  if (proxyConfig.routing?.complexity?.enabled) {
3603
- const complexityModel = proxyConfig.routing?.complexity?.[complexity];
3604
- selectedModel = complexityModel ?? null;
3605
- log(`Complexity routing: ${complexity} → ${selectedModel}`);
3797
+ const complexityVal = proxyConfig.routing?.complexity?.[complexity];
3798
+ if (complexityVal != null) {
3799
+ const parsed = parseComplexityModel(complexityVal);
3800
+ selectedModel = `${parsed.provider}/${parsed.model}`;
3801
+ log(`Complexity routing: ${complexity} → ${parsed.provider}/${parsed.model}`);
3802
+ }
3606
3803
  }
3607
3804
  // Fall back to learned routing rules (non-default only)
3608
3805
  if (!selectedModel) {
@@ -4284,9 +4481,12 @@ async function startProxy(config = {}) {
4284
4481
  else {
4285
4482
  // Complexity-based routing takes priority when enabled
4286
4483
  if (proxyConfig.routing?.complexity?.enabled) {
4287
- const complexityModel = proxyConfig.routing?.complexity?.[complexity];
4288
- selectedModel = complexityModel ?? null;
4289
- log(`Complexity routing: ${complexity} → ${selectedModel}`);
4484
+ const complexityVal = proxyConfig.routing?.complexity?.[complexity];
4485
+ if (complexityVal != null) {
4486
+ const parsed = parseComplexityModel(complexityVal);
4487
+ selectedModel = `${parsed.provider}/${parsed.model}`;
4488
+ log(`Complexity routing: ${complexity} → ${parsed.provider}/${parsed.model}`);
4489
+ }
4290
4490
  }
4291
4491
  // Fall back to learned routing rules (non-default only)
4292
4492
  if (!selectedModel && !targetModel) {