@relayplane/proxy 1.8.23 → 1.8.26

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,119 @@ 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 rawProvider = val.slice(0, idx);
433
+ const model = val.slice(idx + 1); // preserves openrouter/anthropic/claude-... style
434
+ const knownProviders = ['openai', 'anthropic', 'google', 'xai', 'openrouter', 'deepseek', 'groq', 'local', 'ollama'];
435
+ if (!knownProviders.includes(rawProvider)) {
436
+ console.warn(`[parseComplexityModel] Unknown provider "${rawProvider}" in config, falling back to anthropic`);
437
+ return { provider: 'anthropic', model };
438
+ }
439
+ const provider = rawProvider;
440
+ return { provider, model };
441
+ }
442
+ // Plain model name — look up in MODEL_MAPPING, fallback to anthropic
443
+ return exports.MODEL_MAPPING[val] ?? { provider: 'anthropic', model: val };
444
+ }
445
+ return { provider: 'anthropic', model: 'claude-sonnet-4-6' };
446
+ }
447
+ /** Per-provider default complexity tier models */
448
+ const PROVIDER_COMPLEXITY_TIERS = {
449
+ anthropic: {
450
+ simple: { provider: 'anthropic', model: 'claude-haiku-4-5' },
451
+ moderate: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
452
+ complex: { provider: 'anthropic', model: 'claude-opus-4-6' },
453
+ },
454
+ openai: {
455
+ simple: { provider: 'openai', model: 'gpt-4.1-mini' },
456
+ moderate: { provider: 'openai', model: 'gpt-5.4' },
457
+ complex: { provider: 'openai', model: 'gpt-5.4' },
458
+ },
459
+ google: {
460
+ simple: { provider: 'google', model: 'gemini-2.5-flash-lite' },
461
+ moderate: { provider: 'google', model: 'gemini-2.5-flash' },
462
+ complex: { provider: 'google', model: 'gemini-2.5-pro' },
463
+ },
464
+ xai: {
465
+ simple: { provider: 'xai', model: 'grok-4.1-fast' },
466
+ moderate: { provider: 'xai', model: 'grok-4.20-beta' },
467
+ complex: { provider: 'xai', model: 'grok-4' },
468
+ },
469
+ deepseek: {
470
+ simple: { provider: 'deepseek', model: 'deepseek-chat' },
471
+ moderate: { provider: 'deepseek', model: 'deepseek-chat' },
472
+ complex: { provider: 'deepseek', model: 'deepseek-reasoner' },
473
+ },
474
+ openrouter: {
475
+ simple: { provider: 'openrouter', model: 'google/gemini-2.5-flash-lite' },
476
+ moderate: { provider: 'openrouter', model: 'google/gemini-2.5-flash' },
477
+ complex: { provider: 'openrouter', model: 'anthropic/claude-sonnet-4-6' },
478
+ },
479
+ };
480
+ /**
481
+ * Detect which AI providers are available based on env vars and user config.
482
+ * Returns providers in priority order: anthropic > openai > google > xai > deepseek > openrouter > groq
483
+ */
484
+ function detectAvailableProviders(userConfig) {
485
+ const cfg = (userConfig ?? {});
486
+ const auth = (cfg['auth'] ?? {});
487
+ const available = [];
488
+ if (process.env['ANTHROPIC_API_KEY'] || auth['anthropicApiKey'] || auth['anthropicMaxToken']) {
489
+ available.push('anthropic');
490
+ }
491
+ if (process.env['OPENAI_API_KEY'] || auth['openaiApiKey']) {
492
+ available.push('openai');
493
+ }
494
+ if (process.env['GOOGLE_API_KEY'] || process.env['GEMINI_API_KEY'] || auth['googleApiKey']) {
495
+ available.push('google');
496
+ }
497
+ if (process.env['XAI_API_KEY'] || auth['xaiApiKey']) {
498
+ available.push('xai');
499
+ }
500
+ if (process.env['DEEPSEEK_API_KEY'] || auth['deepseekApiKey']) {
501
+ available.push('deepseek');
502
+ }
503
+ if (process.env['OPENROUTER_API_KEY'] || auth['openrouterApiKey']) {
504
+ available.push('openrouter');
505
+ }
506
+ if (process.env['GROQ_API_KEY'] || auth['groqApiKey']) {
507
+ available.push('groq');
508
+ }
509
+ return available;
510
+ }
511
+ /**
512
+ * Build default complexity tiers based on first detected provider.
513
+ * Config overrides win — only fills in tiers not explicitly set.
514
+ */
515
+ function buildDefaultComplexityTiers(providers, existing) {
516
+ // Find first provider that has a known tier mapping
517
+ const primaryProvider = providers.find((p) => PROVIDER_COMPLEXITY_TIERS[p]) ?? 'anthropic';
518
+ const defaults = PROVIDER_COMPLEXITY_TIERS[primaryProvider] ?? PROVIDER_COMPLEXITY_TIERS['anthropic'];
519
+ const simple = existing?.simple != null
520
+ ? parseComplexityModel(existing.simple)
521
+ : defaults.simple;
522
+ const moderate = existing?.moderate != null
523
+ ? parseComplexityModel(existing.moderate)
524
+ : defaults.moderate;
525
+ const complex = existing?.complex != null
526
+ ? parseComplexityModel(existing.complex)
527
+ : defaults.complex;
528
+ return { simple, moderate, complex };
529
+ }
382
530
  const UNCERTAINTY_PATTERNS = [
383
531
  /i'?m not (entirely |completely |really )?sure/i,
384
532
  /i don'?t (really |actually )?know/i,
@@ -1411,6 +1559,44 @@ function convertMessagesToGemini(messages) {
1411
1559
  }
1412
1560
  return geminiContents;
1413
1561
  }
1562
+ /**
1563
+ * Recursively strip JSON Schema properties that Gemini rejects but OpenAI/Anthropic accept.
1564
+ * Gemini rejects: patternProperties, additionalProperties (boolean), $schema, definitions, $defs, unevaluatedProperties
1565
+ */
1566
+ function sanitizeSchemaForGemini(schema, _depth = 0, _nodeCount = { count: 0 }) {
1567
+ // Guard against deeply nested or extremely wide schemas (DoS prevention)
1568
+ if (_depth > 20)
1569
+ return schema;
1570
+ _nodeCount.count++;
1571
+ if (_nodeCount.count > 10000)
1572
+ return schema;
1573
+ if (Array.isArray(schema)) {
1574
+ return schema.map(item => sanitizeSchemaForGemini(item, _depth + 1, _nodeCount));
1575
+ }
1576
+ if (schema !== null && typeof schema === 'object') {
1577
+ const obj = schema;
1578
+ const result = {};
1579
+ for (const [key, value] of Object.entries(obj)) {
1580
+ // Strip fields Gemini doesn't support
1581
+ if (key === 'patternProperties')
1582
+ continue;
1583
+ if (key === '$schema')
1584
+ continue;
1585
+ if (key === 'definitions')
1586
+ continue;
1587
+ if (key === '$defs')
1588
+ continue;
1589
+ if (key === 'unevaluatedProperties')
1590
+ continue;
1591
+ // additionalProperties: Gemini only accepts object form, not boolean
1592
+ if (key === 'additionalProperties' && typeof value === 'boolean')
1593
+ continue;
1594
+ result[key] = sanitizeSchemaForGemini(value, _depth + 1, _nodeCount);
1595
+ }
1596
+ return result;
1597
+ }
1598
+ return schema;
1599
+ }
1414
1600
  /**
1415
1601
  * Forward non-streaming request to Gemini API
1416
1602
  */
@@ -1436,7 +1622,7 @@ async function forwardToGemini(request, targetModel, apiKey) {
1436
1622
  functionDeclarations: request.tools.map((t) => ({
1437
1623
  name: t.function.name,
1438
1624
  description: t.function.description || "",
1439
- parameters: t.function.parameters || {}
1625
+ parameters: sanitizeSchemaForGemini(t.function.parameters || {})
1440
1626
  }))
1441
1627
  }];
1442
1628
  }
@@ -1474,7 +1660,7 @@ async function forwardToGeminiStream(request, targetModel, apiKey) {
1474
1660
  functionDeclarations: request.tools.map((t) => ({
1475
1661
  name: t.function.name,
1476
1662
  description: t.function.description || "",
1477
- parameters: t.function.parameters || {}
1663
+ parameters: sanitizeSchemaForGemini(t.function.parameters || {})
1478
1664
  }))
1479
1665
  }];
1480
1666
  }
@@ -2235,18 +2421,25 @@ function getCooldownConfig(config) {
2235
2421
  };
2236
2422
  return { ...defaults, ...config.reliability?.cooldowns };
2237
2423
  }
2424
+ function complexityValToString(val) {
2425
+ if (val == null)
2426
+ return undefined;
2427
+ if (typeof val === 'string')
2428
+ return val;
2429
+ return `${val.provider}/${val.model}`;
2430
+ }
2238
2431
  function getCostModel(config) {
2239
- return (config.routing?.complexity?.simple ||
2432
+ return (complexityValToString(config.routing?.complexity?.simple) ||
2240
2433
  config.routing?.cascade?.models?.[0] ||
2241
2434
  'claude-haiku-4-5');
2242
2435
  }
2243
2436
  function getFastModel(config) {
2244
- return (config.routing?.complexity?.simple ||
2437
+ return (complexityValToString(config.routing?.complexity?.simple) ||
2245
2438
  config.routing?.cascade?.models?.[0] ||
2246
2439
  'claude-haiku-4-5');
2247
2440
  }
2248
2441
  function getQualityModel(config) {
2249
- return (config.routing?.complexity?.complex ||
2442
+ return (complexityValToString(config.routing?.complexity?.complex) ||
2250
2443
  config.routing?.cascade?.models?.[config.routing?.cascade?.models?.length ? config.routing.cascade.models.length - 1 : 0] ||
2251
2444
  process.env['RELAYPLANE_QUALITY_MODEL'] ||
2252
2445
  'claude-sonnet-4-6');
@@ -2604,61 +2797,72 @@ async function startProxy(config = {}) {
2604
2797
  log(`[CROSS-CASCADE] Enabled. Provider order: ${proxyConfig.crossProviderCascade.providers.join(' → ')}`);
2605
2798
  }
2606
2799
  const isFirstRun = !rawFileHasRouting || !userConfig.first_run_complete;
2800
+ // Always detect available providers and update DEFAULT_ROUTING at startup
2801
+ const availableProviders = detectAvailableProviders(userConfig);
2802
+ {
2803
+ // Build human-readable provider labels for startup log
2804
+ const providerLabels = availableProviders.map((p) => {
2805
+ if (p === 'anthropic') {
2806
+ const key = process.env['ANTHROPIC_API_KEY'] || '';
2807
+ return key.startsWith('sk-ant-api') ? '✓ Anthropic' : '✓ Anthropic (Max)';
2808
+ }
2809
+ return `✓ ${p.charAt(0).toUpperCase() + p.slice(1)}`;
2810
+ });
2811
+ if (providerLabels.length > 0) {
2812
+ console.log(`[RelayPlane] ${providerLabels.join(', ')}`);
2813
+ }
2814
+ // Build default tiers, respecting any existing user config overrides
2815
+ const existingComplexity = proxyConfig.routing?.complexity;
2816
+ const defaultProviders = availableProviders.length > 0 ? availableProviders : ['openrouter'];
2817
+ const tiers = buildDefaultComplexityTiers(defaultProviders, existingComplexity);
2818
+ // Update DEFAULT_ROUTING with detected provider's moderate tier
2819
+ const moderateRoute = tiers.moderate;
2820
+ const allTaskTypes = ['code_generation', 'code_review', 'summarization', 'analysis', 'creative_writing', 'data_extraction', 'translation', 'question_answering', 'general'];
2821
+ for (const tt of allTaskTypes) {
2822
+ DEFAULT_ROUTING[tt] = moderateRoute;
2823
+ }
2824
+ console.log(`[RelayPlane] Auto-routing: simple=${tiers.simple.model}, moderate=${tiers.moderate.model}, complex=${tiers.complex.model}`);
2825
+ }
2607
2826
  if (isFirstRun || proxyConfig.routing?.mode === 'auto') {
2608
2827
  const envAnthropicKey = process.env['ANTHROPIC_API_KEY'];
2609
2828
  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
- },
2829
+ if (isFirstRun) {
2830
+ let existingRaw = {};
2831
+ try {
2832
+ existingRaw = JSON.parse(await fs.promises.readFile(configPath, 'utf8'));
2833
+ }
2834
+ catch { /* fresh start, no existing config */ }
2835
+ let autoComplexity;
2836
+ if (availableProviders.includes('anthropic') && hasRegularApiKey) {
2837
+ // Full Anthropic API key — enable haiku 3-tier routing
2838
+ console.log('[RelayPlane] Auto-config: ANTHROPIC_API_KEY detected enabling 3-tier routing (haiku/sonnet/opus)');
2839
+ autoComplexity = { simple: 'claude-haiku-4-5', moderate: 'claude-sonnet-4-6', complex: 'claude-opus-4-6' };
2840
+ }
2841
+ else if (availableProviders.length > 0 && !availableProviders.includes('anthropic')) {
2842
+ // Non-Anthropic provider — use detected provider's tiers
2843
+ const providerTiers = buildDefaultComplexityTiers(availableProviders);
2844
+ console.log(`[RelayPlane] Auto-config: ${availableProviders[0]} detected — enabling provider-aware 3-tier routing`);
2845
+ autoComplexity = {
2846
+ simple: `${providerTiers.simple.provider}/${providerTiers.simple.model}`,
2847
+ moderate: `${providerTiers.moderate.provider}/${providerTiers.moderate.model}`,
2848
+ complex: `${providerTiers.complex.provider}/${providerTiers.complex.model}`,
2629
2849
  };
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
2850
  }
2636
- }
2637
- else {
2638
- // No regular API key — OAuth only or no Anthropic key; skip Haiku (OAuth not supported for Haiku)
2639
- if (isFirstRun) {
2851
+ else {
2852
+ // OAuth only or no API key — skip Haiku (OAuth not supported for Haiku)
2640
2853
  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)`);
2854
+ autoComplexity = { simple: 'claude-sonnet-4-6', moderate: 'claude-sonnet-4-6', complex: 'claude-opus-4-6' };
2661
2855
  }
2856
+ const autoRouting = {
2857
+ mode: 'complexity',
2858
+ cascade: { enabled: false, models: [], escalateOn: 'uncertainty', maxEscalations: 1 },
2859
+ complexity: { enabled: true, ...autoComplexity },
2860
+ };
2861
+ const updatedConfig = { ...existingRaw, routing: autoRouting, first_run_complete: true };
2862
+ await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
2863
+ await fs.promises.writeFile(configPath, JSON.stringify(updatedConfig, null, 2), 'utf8');
2864
+ proxyConfig = await loadProxyConfig(configPath, log);
2865
+ console.log(`[RelayPlane] Auto-config: wrote routing config to ${configPath}`);
2662
2866
  }
2663
2867
  }
2664
2868
  }
@@ -3193,8 +3397,6 @@ async function startProxy(config = {}) {
3193
3397
  providerStats[provider].success++;
3194
3398
  }
3195
3399
  }
3196
- // Debug: log provider stats
3197
- console.log('[RelayPlane Health] Provider stats:', JSON.stringify(providerStats));
3198
3400
  const providers = [];
3199
3401
  for (const [name, ep] of Object.entries(exports.DEFAULT_ENDPOINTS)) {
3200
3402
  // Skip Ollama from normal key-based health check — it's handled separately
@@ -3558,7 +3760,11 @@ async function startProxy(config = {}) {
3558
3760
  useCascade = false; // Disable full cascade, use complexity routing instead
3559
3761
  let selectedModel = null;
3560
3762
  if (proxyConfig.routing?.complexity?.enabled) {
3561
- selectedModel = proxyConfig.routing?.complexity?.[complexity];
3763
+ const complexityVal = proxyConfig.routing?.complexity?.[complexity];
3764
+ if (complexityVal != null) {
3765
+ const parsed = parseComplexityModel(complexityVal);
3766
+ selectedModel = `${parsed.provider}/${parsed.model}`;
3767
+ }
3562
3768
  }
3563
3769
  else {
3564
3770
  selectedModel = getCascadeModels(proxyConfig)[0] || getCostModel(proxyConfig);
@@ -3600,9 +3806,12 @@ async function startProxy(config = {}) {
3600
3806
  else {
3601
3807
  // Complexity-based routing takes priority when enabled
3602
3808
  if (proxyConfig.routing?.complexity?.enabled) {
3603
- const complexityModel = proxyConfig.routing?.complexity?.[complexity];
3604
- selectedModel = complexityModel ?? null;
3605
- log(`Complexity routing: ${complexity} → ${selectedModel}`);
3809
+ const complexityVal = proxyConfig.routing?.complexity?.[complexity];
3810
+ if (complexityVal != null) {
3811
+ const parsed = parseComplexityModel(complexityVal);
3812
+ selectedModel = `${parsed.provider}/${parsed.model}`;
3813
+ log(`Complexity routing: ${complexity} → ${parsed.provider}/${parsed.model}`);
3814
+ }
3606
3815
  }
3607
3816
  // Fall back to learned routing rules (non-default only)
3608
3817
  if (!selectedModel) {
@@ -4284,9 +4493,12 @@ async function startProxy(config = {}) {
4284
4493
  else {
4285
4494
  // Complexity-based routing takes priority when enabled
4286
4495
  if (proxyConfig.routing?.complexity?.enabled) {
4287
- const complexityModel = proxyConfig.routing?.complexity?.[complexity];
4288
- selectedModel = complexityModel ?? null;
4289
- log(`Complexity routing: ${complexity} → ${selectedModel}`);
4496
+ const complexityVal = proxyConfig.routing?.complexity?.[complexity];
4497
+ if (complexityVal != null) {
4498
+ const parsed = parseComplexityModel(complexityVal);
4499
+ selectedModel = `${parsed.provider}/${parsed.model}`;
4500
+ log(`Complexity routing: ${complexity} → ${parsed.provider}/${parsed.model}`);
4501
+ }
4290
4502
  }
4291
4503
  // Fall back to learned routing rules (non-default only)
4292
4504
  if (!selectedModel && !targetModel) {