freegate 0.6.18 → 0.6.20

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/providers.json CHANGED
@@ -370,5 +370,269 @@
370
370
  "envVar": "PROVIDER_MISTRAL_APIKEY",
371
371
  "free": true,
372
372
  "category": "coding"
373
+ },
374
+ "sambanova-gpt-oss-120b": {
375
+ "endpoint": "https://api.sambanova.ai/v1/chat/completions",
376
+ "model": "gpt-oss-120b",
377
+ "priority": 12,
378
+ "dailyLimit": 100000,
379
+ "keyHint": "cloud.sambanova.ai → API Keys",
380
+ "envVar": "PROVIDER_SAMBANOVA_APIKEY",
381
+ "free": true,
382
+ "category": "general",
383
+ "context_window": 131072
384
+ },
385
+ "sambanova-minimax-m2": {
386
+ "endpoint": "https://api.sambanova.ai/v1/chat/completions",
387
+ "model": "MiniMax-M2.7",
388
+ "priority": 13,
389
+ "dailyLimit": 100000,
390
+ "keyHint": "cloud.sambanova.ai → API Keys",
391
+ "envVar": "PROVIDER_SAMBANOVA_APIKEY",
392
+ "free": true,
393
+ "category": "reasoning",
394
+ "context_window": 131072
395
+ },
396
+ "sambanova-deepseek-v3": {
397
+ "endpoint": "https://api.sambanova.ai/v1/chat/completions",
398
+ "model": "DeepSeek-V3.2",
399
+ "priority": 13,
400
+ "dailyLimit": 100000,
401
+ "keyHint": "cloud.sambanova.ai → API Keys",
402
+ "envVar": "PROVIDER_SAMBANOVA_APIKEY",
403
+ "free": true,
404
+ "category": "reasoning",
405
+ "context_window": 131072
406
+ },
407
+ "sambanova-llama4-maverick": {
408
+ "endpoint": "https://api.sambanova.ai/v1/chat/completions",
409
+ "model": "Llama-4-Maverick-17B-128E-Instruct",
410
+ "priority": 13,
411
+ "dailyLimit": 100000,
412
+ "keyHint": "cloud.sambanova.ai → API Keys",
413
+ "envVar": "PROVIDER_SAMBANOVA_APIKEY",
414
+ "free": true,
415
+ "category": "general",
416
+ "context_window": 1000000
417
+ },
418
+ "sambanova-llama3.3-70b": {
419
+ "endpoint": "https://api.sambanova.ai/v1/chat/completions",
420
+ "model": "Meta-Llama-3.3-70B-Instruct",
421
+ "priority": 14,
422
+ "dailyLimit": 100000,
423
+ "keyHint": "cloud.sambanova.ai → API Keys",
424
+ "envVar": "PROVIDER_SAMBANOVA_APIKEY",
425
+ "free": true,
426
+ "category": "general",
427
+ "context_window": 131072
428
+ },
429
+ "siliconflow-deepseek-r1": {
430
+ "endpoint": "https://api.siliconflow.com/v1/chat/completions",
431
+ "model": "deepseek-ai/DeepSeek-R1",
432
+ "priority": 12,
433
+ "dailyLimit": 100000,
434
+ "keyHint": "cloud.siliconflow.com → API Keys",
435
+ "envVar": "PROVIDER_SILICONFLOW_APIKEY",
436
+ "free": true,
437
+ "category": "reasoning",
438
+ "context_window": 131072
439
+ },
440
+ "siliconflow-qwen3-coder": {
441
+ "endpoint": "https://api.siliconflow.com/v1/chat/completions",
442
+ "model": "Qwen/Qwen3-Coder-480B-A35B-Instruct",
443
+ "priority": 12,
444
+ "dailyLimit": 100000,
445
+ "keyHint": "cloud.siliconflow.com → API Keys",
446
+ "envVar": "PROVIDER_SILICONFLOW_APIKEY",
447
+ "free": true,
448
+ "category": "coding",
449
+ "context_window": 131072
450
+ },
451
+ "siliconflow-qwen3-235b": {
452
+ "endpoint": "https://api.siliconflow.com/v1/chat/completions",
453
+ "model": "Qwen/Qwen3-235B-A22B-Instruct-2507",
454
+ "priority": 13,
455
+ "dailyLimit": 100000,
456
+ "keyHint": "cloud.siliconflow.com → API Keys",
457
+ "envVar": "PROVIDER_SILICONFLOW_APIKEY",
458
+ "free": true,
459
+ "category": "reasoning",
460
+ "context_window": 131072
461
+ },
462
+ "siliconflow-kimi-k2": {
463
+ "endpoint": "https://api.siliconflow.com/v1/chat/completions",
464
+ "model": "moonshotai/Kimi-K2.5",
465
+ "priority": 13,
466
+ "dailyLimit": 100000,
467
+ "keyHint": "cloud.siliconflow.com → API Keys",
468
+ "envVar": "PROVIDER_SILICONFLOW_APIKEY",
469
+ "free": true,
470
+ "category": "general",
471
+ "context_window": 131072
472
+ },
473
+ "siliconflow-glm": {
474
+ "endpoint": "https://api.siliconflow.com/v1/chat/completions",
475
+ "model": "zai-org/GLM-4.7",
476
+ "priority": 14,
477
+ "dailyLimit": 100000,
478
+ "keyHint": "cloud.siliconflow.com → API Keys",
479
+ "envVar": "PROVIDER_SILICONFLOW_APIKEY",
480
+ "free": true,
481
+ "category": "general",
482
+ "context_window": 131072
483
+ },
484
+ "deepinfra-gpt-oss-120b": {
485
+ "endpoint": "https://api.deepinfra.com/v1/openai/chat/completions",
486
+ "model": "openai/gpt-oss-120b",
487
+ "priority": 12,
488
+ "dailyLimit": 100000,
489
+ "keyHint": "deepinfra.com → API Keys",
490
+ "envVar": "PROVIDER_DEEPINFRA_APIKEY",
491
+ "free": true,
492
+ "category": "general",
493
+ "context_window": 131072
494
+ },
495
+ "deepinfra-gpt-oss-20b": {
496
+ "endpoint": "https://api.deepinfra.com/v1/openai/chat/completions",
497
+ "model": "openai/gpt-oss-20b",
498
+ "priority": 13,
499
+ "dailyLimit": 100000,
500
+ "keyHint": "deepinfra.com → API Keys",
501
+ "envVar": "PROVIDER_DEEPINFRA_APIKEY",
502
+ "free": true,
503
+ "category": "general",
504
+ "context_window": 131072
505
+ },
506
+ "deepinfra-nemotron-super-120b": {
507
+ "endpoint": "https://api.deepinfra.com/v1/openai/chat/completions",
508
+ "model": "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B",
509
+ "priority": 12,
510
+ "dailyLimit": 100000,
511
+ "keyHint": "deepinfra.com → API Keys",
512
+ "envVar": "PROVIDER_DEEPINFRA_APIKEY",
513
+ "free": true,
514
+ "category": "reasoning",
515
+ "context_window": 131072
516
+ },
517
+ "deepinfra-llama-4-maverick": {
518
+ "endpoint": "https://api.deepinfra.com/v1/openai/chat/completions",
519
+ "model": "meta-llama/Llama-4-Maverick-17B-128E-Instruct",
520
+ "priority": 14,
521
+ "dailyLimit": 100000,
522
+ "keyHint": "deepinfra.com → API Keys",
523
+ "envVar": "PROVIDER_DEEPINFRA_APIKEY",
524
+ "free": true,
525
+ "category": "general",
526
+ "context_window": 1000000
527
+ },
528
+ "hyperbolic-deepseek-v3": {
529
+ "endpoint": "https://api.hyperbolic.xyz/v1/chat/completions",
530
+ "model": "deepseek-ai/DeepSeek-V3",
531
+ "priority": 14,
532
+ "dailyLimit": 100000,
533
+ "keyHint": "app.hyperbolic.xyz → API Keys",
534
+ "envVar": "PROVIDER_HYPERBOLIC_APIKEY",
535
+ "free": true,
536
+ "category": "reasoning",
537
+ "context_window": 131072
538
+ },
539
+ "hyperbolic-qwen3-coder": {
540
+ "endpoint": "https://api.hyperbolic.xyz/v1/chat/completions",
541
+ "model": "Qwen/Qwen2.5-Coder-32B-Instruct",
542
+ "priority": 14,
543
+ "dailyLimit": 100000,
544
+ "keyHint": "app.hyperbolic.xyz → API Keys",
545
+ "envVar": "PROVIDER_HYPERBOLIC_APIKEY",
546
+ "free": true,
547
+ "category": "coding",
548
+ "context_window": 131072
549
+ },
550
+ "hyperbolic-llama-70b": {
551
+ "endpoint": "https://api.hyperbolic.xyz/v1/chat/completions",
552
+ "model": "meta-llama/Llama-3.3-70B-Instruct",
553
+ "priority": 15,
554
+ "dailyLimit": 100000,
555
+ "keyHint": "app.hyperbolic.xyz → API Keys",
556
+ "envVar": "PROVIDER_HYPERBOLIC_APIKEY",
557
+ "free": true,
558
+ "category": "general",
559
+ "context_window": 131072
560
+ },
561
+ "cohere-command-a": {
562
+ "endpoint": "https://api.cohere.com/compatibility/v1/chat/completions",
563
+ "model": "command-a-03-2025",
564
+ "priority": 15,
565
+ "dailyLimit": 100000,
566
+ "keyHint": "dashboard.cohere.com → API Keys",
567
+ "envVar": "PROVIDER_COHERE_APIKEY",
568
+ "free": true,
569
+ "category": "reasoning",
570
+ "context_window": 131072
571
+ },
572
+ "cohere-command-r-plus": {
573
+ "endpoint": "https://api.cohere.com/compatibility/v1/chat/completions",
574
+ "model": "command-r-plus-08-2024",
575
+ "priority": 16,
576
+ "dailyLimit": 100000,
577
+ "keyHint": "dashboard.cohere.com → API Keys",
578
+ "envVar": "PROVIDER_COHERE_APIKEY",
579
+ "free": true,
580
+ "category": "general",
581
+ "context_window": 131072
582
+ },
583
+ "llm7-gpt4o-mini": {
584
+ "endpoint": "https://api.llm7.io/v1/chat/completions",
585
+ "model": "gpt-4o-mini-2024-07-18",
586
+ "priority": 13,
587
+ "dailyLimit": 100000,
588
+ "keyHint": "llm7.io (бесплатный ключ при регистрации)",
589
+ "envVar": "PROVIDER_LLM7_APIKEY",
590
+ "free": true,
591
+ "category": "general",
592
+ "context_window": 131072
593
+ },
594
+ "llm7-deepseek-r1": {
595
+ "endpoint": "https://api.llm7.io/v1/chat/completions",
596
+ "model": "deepseek-r1-0528",
597
+ "priority": 13,
598
+ "dailyLimit": 100000,
599
+ "keyHint": "llm7.io (бесплатный ключ при регистрации)",
600
+ "envVar": "PROVIDER_LLM7_APIKEY",
601
+ "free": true,
602
+ "category": "reasoning",
603
+ "context_window": 131072
604
+ },
605
+ "nara-mistral-large": {
606
+ "endpoint": "https://router.bynara.id/v1/chat/completions",
607
+ "model": "mistral-large",
608
+ "priority": 13,
609
+ "dailyLimit": 100000,
610
+ "keyHint": "nara — бесплатный ключ через Telegram-канал",
611
+ "envVar": "PROVIDER_NARA_APIKEY",
612
+ "free": true,
613
+ "category": "general",
614
+ "context_window": 252000
615
+ },
616
+ "nara-mistral-medium": {
617
+ "endpoint": "https://router.bynara.id/v1/chat/completions",
618
+ "model": "mistral-medium-3-5",
619
+ "priority": 14,
620
+ "dailyLimit": 100000,
621
+ "keyHint": "nara — бесплатный ключ через Telegram-канал",
622
+ "envVar": "PROVIDER_NARA_APIKEY",
623
+ "free": true,
624
+ "category": "general",
625
+ "context_window": 256000
626
+ },
627
+ "nara-tencent-hy3": {
628
+ "endpoint": "https://router.bynara.id/v1/chat/completions",
629
+ "model": "tencent-hy3",
630
+ "priority": 14,
631
+ "dailyLimit": 100000,
632
+ "keyHint": "nara — бесплатный ключ через Telegram-канал",
633
+ "envVar": "PROVIDER_NARA_APIKEY",
634
+ "free": true,
635
+ "category": "general",
636
+ "context_window": 1000000
373
637
  }
374
638
  }
package/server.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { LRUCache } = require('./lib/cache');
6
6
  const { PROVIDERS, MODEL_MAP, callProvider, reloadProviders } = require('./lib/providers');
7
- const { loadState, initHealth, isCircuitOpen, recordSuccess, recordFailure, recordRequest, recordTokens, getHealth, getStats, getReliability, recordRecent, recordRpm, getRecent, getRpm, recordSelection, getLastSelection, getBandit, recordBandit, warmBanditPriors, getContextStats } = require('./lib/health');
7
+ const { loadState, initHealth, isCircuitOpen, recordSuccess, recordFailure, recordRequest, recordTokens, getHealth, getStats, getReliability, recordRecent, recordRpm, getRecent, getRpm, recordSelection, getLastSelection, getBandit, recordBandit, warmBanditPriors, getContextStats, getHourly } = require('./lib/health');
8
8
  const { checkRateLimit } = require('./lib/rateLimit');
9
9
  const { handleDashboard } = require('./lib/dashboard');
10
10
  const { acquire, stats: poolStats } = require('./lib/pool');
@@ -99,6 +99,30 @@ const METHODOLOGY_CONFIG = Object.assign(
99
99
  (config.methodology && typeof config.methodology === 'object') ? config.methodology : {}
100
100
  );
101
101
 
102
+ // --- Самопроверка ответа второй моделью (vetting) ---
103
+ // Опционально: после не-stream ответа отправляем краткий чек другой модели.
104
+ // Выключено по умолчанию (жжёт 2-й free-лимит). Включается config.vetting.enabled.
105
+ const { shouldVet, vetAnswer, VETTING_DEFAULTS } = require('./lib/vetting');
106
+ const VETTING_CONFIG = Object.assign(
107
+ { ...VETTING_DEFAULTS },
108
+ (config.vetting && typeof config.vetting === 'object') ? config.vetting : {}
109
+ );
110
+
111
+ // --- Стратегия роутинга ---
112
+ // Опциональные модификаторы равномерности (round-robin / least-used).
113
+ // По умолчанию 'weighted' — поведение без изменений.
114
+ const { makeWeightModifier } = require('./lib/strategy');
115
+ const ROUTING_STRATEGY = (config.routing && config.routing.strategy) || 'weighted';
116
+
117
+ // --- Сжатие промпта (Caveman-стиль) ---
118
+ // Опционально убирает вежливость/заполнители из последнего user-сообщения,
119
+ // экономя токены. config.compress.enabled=true включает.
120
+ const { compressMessages } = require('./lib/compress');
121
+ const COMPRESS_CONFIG = Object.assign(
122
+ { enabled: false, minLen: 60 },
123
+ (config.compress && typeof config.compress === 'object') ? config.compress : {}
124
+ );
125
+
102
126
  // --- Веб-поиск для search-задач ---
103
127
  // Бесплатный поиск фактов (DuckDuckGo, без ключа) для запросов-поиска, чтобы
104
128
  // модель не галлюцинировала («что такое минимакс дизайн» → реальная инфа про
@@ -459,6 +483,15 @@ async function handleChatCompletion(req, res, body) {
459
483
  if (METHODOLOGY_CONFIG.enabled && Array.isArray(body.messages)) {
460
484
  try {
461
485
  taskCategory = classifyTask(body.messages);
486
+ // Сжатие промпта: убираем вежливость/заполнители ДО методолога (методолог
487
+ // не должен суммировать сжатый текст). Опционально (config.compress).
488
+ if (COMPRESS_CONFIG.enabled) {
489
+ const compressed = compressMessages(body.messages, COMPRESS_CONFIG);
490
+ if (compressed !== body.messages && Array.isArray(compressed)) {
491
+ body.messages = compressed;
492
+ measure.compressed = 1;
493
+ }
494
+ }
462
495
  const injected = injectMethodology(body.messages, taskCategory, METHODOLOGY_CONFIG);
463
496
  if (injected !== body.messages) {
464
497
  body.messages = injected;
@@ -608,6 +641,9 @@ async function handleChatCompletion(req, res, body) {
608
641
  .sort((a, b) => (b.provider.context_window || 0) - (a.provider.context_window || 0))
609
642
  .slice(0, 1);
610
643
  } else {
644
+ const usedTodayList = {};
645
+ for (const [k] of pool) usedTodayList[k] = usedTodayFor(k);
646
+ const strategyModifier = makeWeightModifier(ROUTING_STRATEGY, { keys: pool.map(([k]) => k), usedTodayList });
611
647
  const scored = pool.map(([key, provider]) => {
612
648
  const h = getHealth()[key];
613
649
  let score = h.score || 50;
@@ -640,6 +676,9 @@ async function handleChatCompletion(req, res, body) {
640
676
  const usedToday = usedTodayFor(key);
641
677
  if (dailyLimit > 0 && usedToday >= dailyLimit) weight *= 0.03;
642
678
  else if (dailyLimit > 0 && usedToday >= dailyLimit * 0.9) weight *= 0.4;
679
+ // Стратегия роутинга: равномерность (round-robin / least-used) как
680
+ // лёгкий модификатор к базовому weight — не ломает основной скоринг.
681
+ weight *= strategyModifier(key);
643
682
  return { key, provider, weight };
644
683
  });
645
684
 
@@ -836,7 +875,7 @@ async function handleChatCompletion(req, res, body) {
836
875
  model: provider.model,
837
876
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
838
877
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
839
- });
878
+ }, key);
840
879
  }
841
880
  commit(200);
842
881
  res.end();
@@ -946,7 +985,7 @@ async function handleChatCompletion(req, res, body) {
946
985
  model: provider.model,
947
986
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
948
987
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
949
- });
988
+ }, key);
950
989
  }
951
990
  commit(200);
952
991
  res.end();
@@ -971,8 +1010,31 @@ async function handleChatCompletion(req, res, body) {
971
1010
  measure.provider = key;
972
1011
  measure.real = (result.data.usage && result.data.usage.prompt_tokens) ? result.data.usage.prompt_tokens : measure.sentTokens || 0;
973
1012
  measure.win = PROVIDERS[key]?.context_window || 0;
1013
+ // Самопроверка второй моделью (опционально): только не-stream, только по конфигу.
1014
+ if (VETTING_CONFIG.enabled && !isStreaming) {
1015
+ const answer = result.data?.choices?.[0]?.message?.content || '';
1016
+ if (shouldVet({ config: VETTING_CONFIG, complexity, category: taskCategory, answerLen: answer.length })) {
1017
+ const picks = Object.entries(PROVIDERS)
1018
+ .filter(([pk, p]) => p.enabled && pk !== key && !isCircuitOpen(pk) &&
1019
+ getHealth()[pk]?.status === 'up' && p.vision !== true)
1020
+ .map(([_, p]) => p);
1021
+ try {
1022
+ const verdict = await vetAnswer({ answer, callProvider, picks, config: VETTING_CONFIG });
1023
+ if (verdict.checked && !verdict.ok && verdict.note) {
1024
+ const msg = result.data.choices[0].message;
1025
+ msg.content = (msg.content || '') + '\n\n> ⚠️ Проверка второй моделью: ' + verdict.note;
1026
+ measure.vetted = 1;
1027
+ measure.vetNote = verdict.note;
1028
+ } else {
1029
+ measure.vetted = 0;
1030
+ }
1031
+ } catch (vetErr) {
1032
+ measure.vetted = 0;
1033
+ }
1034
+ }
1035
+ }
974
1036
  commit(200);
975
- cache.set(effectiveModel, body.messages, body.temperature, result.data);
1037
+ cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
976
1038
  recordTokens(key, result.usage);
977
1039
  res.writeHead(200, { 'Content-Type': 'application/json' });
978
1040
  res.end(JSON.stringify(result.data));
@@ -1059,7 +1121,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
1059
1121
  measure.real = (result.data.usage && result.data.usage.prompt_tokens) ? result.data.usage.prompt_tokens : measure.sentTokens || 0;
1060
1122
  measure.win = PROVIDERS[key]?.context_window || 0;
1061
1123
  commit(200);
1062
- cache.set(effectiveModel, body.messages, body.temperature, result.data);
1124
+ cache.set(effectiveModel, body.messages, body.temperature, result.data, key);
1063
1125
  recordTokens(key, result.usage);
1064
1126
  res.writeHead(200, { 'Content-Type': 'application/json' });
1065
1127
  res.end(JSON.stringify(result.data));
@@ -1117,7 +1179,7 @@ if (isTooShort(result.data, lastUserText(body.messages))) {
1117
1179
  model: provider.model,
1118
1180
  choices: [{ index: 0, message: { role: 'assistant', content: full }, finish_reason: 'stop' }],
1119
1181
  usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1120
- });
1182
+ }, key);
1121
1183
  }
1122
1184
  commit(200);
1123
1185
  res.end();
@@ -1346,6 +1408,7 @@ const server = http.createServer(async (req, res) => {
1346
1408
  return [k, { status: v.status, score: v.score, latency_ms: v.latency, reason, reliability }];
1347
1409
  })),
1348
1410
  cache: cache.stats(),
1411
+ hourly: (() => { try { return getHourly(); } catch { return []; } })(),
1349
1412
  limits,
1350
1413
  pool: poolStats(),
1351
1414
  last_selection: getLastSelection(),