Пред финальный рабочий код и README.md.

⚡️ Cyber Router v2.6 — Final Integrated Version

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
import os
import json
import asyncio
import threading
import time
import httpx
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from fastapi import FastAPI, Request, Form, HTTPException, BackgroundTasks
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
import uvicorn
import webview
from jinja2 import Template

# --- КОНСТАНТЫ И ХРАНИЛИЩЕ ---
MODELS_FILE = "models.json"
FAILED_FILE = "models.failed.json"
STATS_FILE = "stats.json"
CONFIG_FILE = "config.json"
file_lock = threading.Lock()

app = FastAPI(title="Cyber Router Core")

# --- СЛУЖЕБНЫЕ ФУНКЦИИ ---
def load_json(filename: str, default: Any) -> Any:
    with file_lock:
        try:
            if os.path.exists(filename):
                with open(filename, "r", encoding="utf-8") as f:
                    return json.load(f)
        except Exception: pass
    return default

def save_json(filename: str, data: Any):
    with file_lock:
        try:
            with open(filename, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
        except Exception: pass

def get_config(): 
    return load_json(CONFIG_FILE, {"OpenRouter": "", "OpenCode": "", "Kilo AI": "", "custom_endpoint": ""})

def save_config(config: Dict): 
    save_json(CONFIG_FILE, config)

def get_stats(): 
    return load_json(STATS_FILE, {"success_count": 0, "fail_count": 0, "last_ttok": 0.0, "should_play_sound": False})

def save_stats(**kwargs):
    stats = get_stats()
    stats.update(kwargs)
    save_json(STATS_FILE, stats)

# --- ЛОГИКА РОУТИНГА ---
def calculate_priority(model_id: str, name: str) -> int:
    text = (model_id + " " + name).lower()
    if any(x in text for x in ["70b", "405b", "pro", "coder", "deepseek-v3", "r1"]): return 100
    if any(x in text for x in ["8b", "12b", "14b", "flash", "gemma-2"]): return 50
    return 10

async def fetch_all_free_models():
    config = get_config()
    endpoints = {
        "OpenRouter": {"url": "https://openrouter.ai/api/v1/models", "base": "https://openrouter.ai/api/v1", "key": config.get("OpenRouter")},
        "OpenCode": {"url": "https://opencode.ai/api/v1/models", "base": "https://opencode.ai/api/v1", "key": config.get("OpenCode")},
        "Kilo AI": {"url": "https://kilo.ai/api/v1/models", "base": "https://kilo.ai/api/v1", "key": config.get("Kilo AI")},
    }
    collected = []
    async with httpx.AsyncClient(timeout=10) as client:
        for provider, info in endpoints.items():
            try:
                headers = {"Authorization": f"Bearer {info['key']}"} if info['key'] else {}
                response = await client.get(info["url"], headers=headers)
                if response.status_code != 200: continue
                data = response.json()
                models_list = data.get("data", []) if isinstance(data, dict) else data
                for m in models_list:
                    if not isinstance(m, dict): continue
                    m_id = m.get("id", m.get("name", ""))
                    m_name = m.get("name", m_id)
                    is_free = any([":free" in m_id.lower(), "free" in m_name.lower(), m.get("is_free") is True, m.get("price") == 0])
                    if is_free and m_id:
                        collected.append({
                            "id": m_id, 
                            "name": f"{m_name} [{provider}]", 
                            "baseUrl": info["base"], 
                            "priority": calculate_priority(m_id, m_name), 
                            "provider": provider
                        })
            except Exception: pass
    save_json(MODELS_FILE, {"updated_at": datetime.utcnow().isoformat(), "models": collected})

def get_sorted_available_models():
    data = load_json(MODELS_FILE, {"models": []})
    failed = load_json(FAILED_FILE, {}).get("failed_models", {})
    now = datetime.utcnow()
    available = [m for m in data["models"] if m["id"] not in failed or now >= (datetime.fromisoformat(failed[m["id"]].replace("Z", "")) + timedelta(hours=3))]
    available.sort(key=lambda x: x.get("priority", 10), reverse=True)
    return available

# --- BACKGROUND HEARTBEAT ---
async def auto_scan_loop():
    """Фоновый цикл автоматического обновления списка моделей каждые 30 минут"""
    while True:
        try:
            print(f"[{datetime.now().strftime('%H:%M:%S')}] 🔄 Auto-scanning free models...")
            await fetch_all_free_models()
            print(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ Sync complete.")
        except Exception as e:
            print(f"❌ Auto-scan error: {e}")
        await asyncio.sleep(1800)

@app.on_event("startup")
async def startup_event():
    # Запускаем фоновый сканер при старте сервера
    asyncio.create_task(auto_scan_loop())

# --- UI TEMPLATE ---
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Cyber Router OS</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&display=swap');
        body {
            font-family: 'Fira Code', monospace;
            background: radial-gradient(circle at 50% 0%, #0f172a 0%, #020617 100%);
            color: #e2e8f0;
            overflow: hidden;
        }
        .glass { background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(12px); border: 1px solid rgba(51, 65, 85, 0.5); }
        .neon-cyan { box-shadow: 0 0 15px rgba(6, 182, 212, 0.3); border: 1px solid rgba(6, 182, 212, 0.5); }
        .neon-rose { box-shadow: 0 0 15px rgba(244, 63, 94, 0.3); border: 1px solid rgba(244, 63, 94, 0.5); }
        .tab-active { color: #22d3ee; border-bottom: 2px solid #22d3ee; text-shadow: 0 0 8px #22d3ee; }
        .input-cyber { background: rgba(2, 6, 23, 0.8); border: 1px solid #334155; color: #22d3ee; transition: all 0.3s; }
        .input-cyber:focus { border-color: #06b6d4; box-shadow: 0 0 10px rgba(6, 182, 212, 0.4); outline: none; }
        .animate-glow { animation: glow 2s infinite alternate; }
        @keyframes glow { from { opacity: 0.5; } to { opacity: 1; } }
        .custom-scrollbar::-webkit-scrollbar { width: 4px; }
        .custom-scrollbar::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 10px; }
    </style>
</head>
<body class="h-screen flex flex-col p-6">
    <div class="max-w-6xl mx-auto w-full h-full flex flex-col">
        <header class="flex justify-between items-center mb-6 pb-4 border-b border-slate-800/60">
            <div>
                <h1 class="text-2xl font-bold tracking-tighter text-cyan-400 animate-pulse">⚡ CYBER ROUTER <span class="text-slate-500 text-sm font-normal">v2.6</span></h1>
                <p class="text-[10px] text-slate-500 mt-1 uppercase tracking-widest flex items-center gap-2">
                    <span class="w-2 h-2 rounded-full bg-cyan-500 animate-ping"></span>
                    System status: <span class="text-cyan-300">Operational</span> | 
                    Last Sync: <span class="text-slate-300">{{ last_sync }}</span>
                </p>
            </div>
            <div class="flex gap-3">
                <form action="/refresh" method="post"><button class="neon-cyan bg-slate-900 text-cyan-400 text-xs px-4 py-2 rounded hover:bg-cyan-500 hover:text-black transition">🔄 SCAN MODELS</button></form>
                <form action="/reset" method="post"><button class="neon-rose bg-slate-900 text-rose-400 text-xs px-4 py-2 rounded hover:bg-rose-500 hover:text-black transition">💀 WIPE BANS</button></form>
            </div>
        </header>
        <nav class="flex gap-8 mb-6 text-xs font-semibold uppercase tracking-widest text-slate-500">
            <button onclick="showTab('dashboard')" id="btn-dashboard" class="tab-active pb-2 transition-all">Dashboard</button>
            <button onclick="showTab('settings')" id="btn-settings" class="pb-2 transition-all hover:text-cyan-400">Settings</button>
            <button onclick="showTab('stats')" id="btn-stats" class="pb-2 transition-all hover:text-cyan-400">System Stats</button>
        </nav>
        <div class="flex-1 overflow-hidden relative">
            <!-- TAB: DASHBOARD -->
            <div id="tab-dashboard" class="tab-content h-full flex flex-col gap-6">
                <div class="grid grid-cols-4 gap-4">
                    <div class="glass p-4 rounded-lg">
                        <p class="text-[10px] text-slate-500 uppercase">Available</p>
                        <p class="text-2xl font-bold text-cyan-400">{{ models_count }}</p>
                    </div>
                    <div class="glass p-4 rounded-lg border-l-4 border-l-emerald-500">
                        <p class="text-[10px] text-slate-500 uppercase">Success</p>
                        <p class="text-2xl font-bold text-emerald-400">{{ success_count }}</p>
                    </div>
                    <div class="glass p-4 rounded-lg border-l-4 border-l-rose-500">
                        <p class="text-[10px] text-slate-500 uppercase">Failures</p>
                        <p class="text-2xl font-bold text-rose-400">{{ fail_count }}</p>
                    </div>
                    <div class="glass p-4 rounded-lg border-l-4 border-l-yellow-500">
                        <p class="text-[10px] text-slate-500 uppercase">Last TTOK</p>
                        <p class="text-2xl font-bold text-yellow-400">{{ last_ttok if last_ttok > 0 else '--' }} <span class="text-xs">t/s</span></p>
                    </div>
                </div>
                <div class="flex-1 overflow-y-auto pr-2 custom-scrollbar">
                    <div class="grid grid-cols-2 gap-4">
                        {% for m in models %}
                        <div class="glass p-4 rounded-xl flex flex-col justify-between h-32 transition-all {% if m.in_cooldown %} opacity-50 grayscale neon-rose {% else %} neon-cyan hover:scale-[1.02] {% endif %}">
                            <div class="flex justify-between items-start">
                                <div class="flex items-center">
                                    {% if m.in_cooldown %}<span class="text-rose-500 mr-2">❌</span>{% else %}<span class="text-emerald-500 mr-2">✅</span>{% endif %}
                                    <h3 class="text-xs font-bold truncate max-w-[200px]">{{ m.name }}</h3>
                                </div>
                                <span class="text-[10px] bg-slate-800 text-yellow-500 px-2 py-0.5 rounded border border-yellow-900/50">PTY: {{ m.priority }}</span>
                            </div>
                            <div class="flex justify-between items-center mt-4">
                                <span class="text-[9px] text-slate-500 uppercase">{{ m.provider }}</span>
                                {% if m.in_cooldown %}
                                <form action="/amnesty" method="post"><input type="hidden" name="model_id" value="{{ m.id }}"><button class="text-[9px] bg-rose-900/30 text-rose-400 border border-rose-800 px-2 py-1 rounded hover:bg-rose-600 hover:text-white transition">AMNESTY</button></form>
                                {% else %}
                                <form action="/fail-test" method="post"><input type="hidden" name="model_id" value="{{ m.id }}"><button class="text-[9px] bg-slate-800 text-slate-400 px-2 py-1 rounded hover:bg-rose-600 hover:text-white transition">TEST FAIL</button></form>
                                {% endif %}
                            </div>
                        </div>
                        {% endfor %}
                    </div>
                </div>
            </div>
            <!-- TAB: SETTINGS -->
            <div id="tab-settings" class="tab-content h-full hidden">
                <div class="max-w-2xl glass p-8 rounded-2xl mx-auto">
                    <h2 class="text-lg font-bold text-cyan-400 mb-6 uppercase tracking-widest">⚙️ Configuration</h2>
                    <form action="/save-config" method="post" class="space-y-5">
                        <div class="grid grid-cols-1 gap-4">
                            <div>
                                <label class="text-[10px] text-slate-500 block mb-1 uppercase">OpenRouter API Key</label>
                                <input type="text" name="OpenRouter" value="{{ config['OpenRouter'] }}" class="w-full p-3 rounded input-cyber text-sm">
                            </div>
                            <div>
                                <label class="text-[10px] text-slate-500 block mb-1 uppercase">OpenCode API Key</label>
                                <input type="text" name="OpenCode" value="{{ config['OpenCode'] }}" class="w-full p-3 rounded input-cyber text-sm">
                            </div>
                            <div>
                                <label class="text-[10px] text-slate-500 block mb-1 uppercase">Kilo AI API Key</label>
                                <input type="text" name="Kilo AI" value="{{ config['Kilo AI'] }}" class="w-full p-3 rounded input-cyber text-sm">
                            </div>
                            <div>
                                <label class="text-[10px] text-slate-500 block mb-1 uppercase">Custom Endpoint (v1)</label>
                                <input type="text" name="custom_endpoint" value="{{ config['custom_endpoint'] }}" placeholder="http://localhost:11434/v1" class="w-full p-3 rounded input-cyber text-sm">
                            </div>
                        </div>
                        <button type="submit" class="w-full py-3 bg-cyan-600 text-white font-bold rounded-lg hover:bg-cyan-500 transition shadow-lg shadow-cyan-900/40 uppercase tracking-widest text-xs">Apply Configuration</button>
                    </form>
                </div>
            </div>
            <!-- TAB: STATS -->
            <div id="tab-stats" class="tab-content h-full hidden">
                <div class="grid grid-cols-2 gap-6 h-full">
                    <div class="glass p-6 rounded-2xl flex flex-col justify-center items-center text-center">
                        <p class="text-slate-500 uppercase text-xs mb-2">Total Successful Requests</p>
                        <p class="text-6xl font-bold text-emerald-400 animate-glow">{{ success_count }}</p>
                    </div>
                    <div class="glass p-6 rounded-2xl flex flex-col justify-center items-center text-center">
                        <p class="text-slate-500 uppercase text-xs mb-2">Total Route Failures</p>
                        <p class="text-6xl font-bold text-rose-400 animate-glow">{{ fail_count }}</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
        function showTab(tabId) {
            document.querySelectorAll('.tab-content').forEach(t => t.classList.add('hidden'));
            document.getElementById('tab-' + tabId).classList.remove('hidden');
            document.querySelectorAll('nav button').forEach(b => b.classList.remove('tab-active'));
            document.getElementById('btn-' + tabId).classList.add('tab-active');
        }
        function playGlitchAlarm() {
            try {
                const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                const osc1 = audioCtx.createOscillator();
                const gain1 = audioCtx.createGain();
                osc1.type = 'sawtooth';
                osc1.frequency.setValueAtTime(150, audioCtx.currentTime);
                osc1.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 0.3);
                gain1.gain.setValueAtTime(0.3, audioCtx.currentTime);
                gain1.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
                osc1.connect(gain1); gain1.connect(audioCtx.destination);
                osc1.start(); osc1.stop(audioCtx.currentTime + 0.3);
            } catch(e) {}
        }
        {% if should_play_sound %}
            playGlitchAlarm();
            fetch('/clear-sound', { method: 'POST' });
        {% endif %}
        setTimeout(() => { window.location.reload(); }, 10000);
    </script>
</body>
</html>
"""

# --- ENDPOINTS ---
@app.get("/", response_class=HTMLResponse)
async def dashboard():
    models_data_raw = load_json(MODELS_FILE, {"models": [], "updated_at": ""})
    models_data = models_data_raw["models"]

    last_sync_str = "Never"
    if models_data_raw.get("updated_at"):
        try:
            last_time = datetime.fromisoformat(models_data_raw["updated_at"])
            diff = datetime.utcnow() - last_time
            if diff.seconds < 60: last_sync_str = "Just now"
            elif diff.seconds < 3600: last_sync_str = f"{diff.seconds // 60}m ago"
            else: last_sync_str = f"{diff.days}d ago"
        except: pass

    failed_models = load_json(FAILED_FILE, {}).get("failed_models", {})
    now = datetime.utcnow()
    processed = []
    for m in models_data:
        m_id = m["id"]
        in_cooldown, remains_str = False, ""
        if m_id in failed_models:
            try:
                cooldown_end = datetime.fromisoformat(failed_models[m_id].replace("Z", "")) + timedelta(hours=3)
                if now < cooldown_end:
                    in_cooldown = True
                    mins = int((cooldown_end - now).total_seconds() // 60)
                    remains_str = f"{mins // 60}h {mins % 60}m" if mins > 60 else f"{mins}m"
            except: pass
        processed.append({**m, "in_cooldown": in_cooldown, "remains": remains_str})

    processed.sort(key=lambda x: (not x["in_cooldown"], x["priority"]), reverse=True)
    stats = get_stats()
    return HTMLResponse(content=Template(HTML_TEMPLATE).render(
        models=processed, models_count=len(processed),
        success_count=stats["success_count"], fail_count=stats["fail_count"],
        last_ttok=stats.get("last_ttok", 0.0), should_play_sound=stats.get("should_play_sound", False),
        config=get_config(), last_sync=last_sync_str
    ))

@app.post("/save-config")
async def save_config_endpoint(request: Request):
    form_data = await request.form()
    save_config({k: v for k, v in form_data.items()})
    return RedirectResponse(url="/", status_code=303)

@app.post("/refresh")
async def refresh_models(background_tasks: BackgroundTasks):
    background_tasks.add_task(fetch_all_free_models)
    return RedirectResponse(url="/", status_code=303)

@app.post("/reset")
async def reset_cooldowns():
    save_json(FAILED_FILE, {"failed_models": {}})
    return RedirectResponse(url="/", status_code=303)

@app.post("/clear-sound")
async def clear_sound():
    save_stats(should_play_sound=False)
    return {"status": "ok"}

@app.post("/fail-test")
async def fail_test(model_id: str = Form(...)):
    failed = load_json(FAILED_FILE, {}).get("failed_models", {})
    failed[model_id] = datetime.utcnow().isoformat() + "Z"
    save_json(FAILED_FILE, {"failed_models": failed})
    save_stats(fail=get_stats()["fail_count"] + 1, should_play_sound=True)
    return RedirectResponse(url="/", status_code=303)

@app.post("/amnesty")
async def amnesty_model(model_id: str = Form(...)):
    failed = load_json(FAILED_FILE, {}).get("failed_models", {})
    if model_id in failed: del failed[model_id]
    save_json(FAILED_FILE, {"failed_models": failed})
    return RedirectResponse(url="/", status_code=303)

@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
    body = await request.json()
    is_stream = body.get("stream", False)
    async with httpx.AsyncClient(timeout=30.0) as client:
        while True:
            available = get_sorted_available_models()
            if not available: raise HTTPException(status_code=503, detail="All models in cooldown")
            target = available[0]
            model_id, base_url, provider = target["id"], target["baseUrl"], target["provider"]
            body["model"] = model_id
            config = get_config()
            headers = {"Content-Type": "application/json"}
            if provider in config and config[provider]: headers["Authorization"] = f"Bearer {config[provider]}"
            start_time = time.time()
            try:
                if is_stream:
                    async with client.stream("POST", f"{base_url}/chat/completions", json=body, headers=headers) as response:
                        if response.status_code != 200: raise httpx.HTTPStatusError("Error", request=None, response=response)
                        async def stream_generator():
                            token_count = 0
                            async for chunk in response.aiter_bytes():
                                if chunk:
                                    if b"data:" in chunk: token_count += 1
                                    yield chunk
                            elapsed = time.time() - start_time
                            if elapsed > 0: save_stats(success=get_stats()["success_count"] + 1, last_ttok=token_count / elapsed)
                        return StreamingResponse(stream_generator(), media_type="text/event-stream")
                else:
                    response = await client.post(f"{base_url}/chat/completions", json=body, headers=headers)
                    if response.status_code != 200: raise httpx.HTTPStatusError("Error", request=None, response=response)
                    elapsed = time.time() - start_time
                    data = response.json()
                    tokens = data.get("usage", {}).get("total_tokens", 50)
                    save_stats(success=get_stats()["success_count"] + 1, last_ttok=tokens / elapsed if elapsed > 0 else 0)
                    return data
            except Exception:
                failed = load_json(FAILED_FILE, {}).get("failed_models", {})
                failed[model_id] = datetime.utcnow().isoformat() + "Z"
                save_json(FAILED_FILE, {"failed_models": failed})
                save_stats(fail=get_stats()["fail_count"] + 1, should_play_sound=True)
                continue

@app.get("/v1/models")
async def list_models():
    return {"object": "list", "data": [{"id": "cyber-free", "object": "model", "created": int(time.time()), "owned_by": "cyber-router"}]}

def start_fastapi():
    uvicorn.run(app, host="127.0.0.1", port=8000, log_level="warning")

if __name__ == "__main__":
    t = threading.Thread(target=start_fastapi, daemon=True)
    t.start()
    time.sleep(1)
    webview.create_window(title="⚡ Cyber Router OS v2.6", url="http://127.0.0.1:8000", width=1150, height=750, resizable=True)
    webview.start()

📄 README.MD

# ⚡ Cyber Router v2.6

**Cyber Router** — это легковесный локальный middleware-прокси и интеллектуальный агрегатор бесплатных ИИ-моделей. Приложение маскируется под официальный сервер OpenAI API, позволяя вам использовать любые LLM-интерфейсы (Cursor, Open WebUI, AnythingLLM), переключаясь между бесплатными провайдерами автоматически.

## 🚀 Основные возможности
- **Автоматический Failover:** Если одна модель выдает ошибку 429 (Limit Exceeded) или падает по таймауту, роутер мгновенно переключает запрос на следующую доступную модель из списка приоритетов.
- **Умная сортировка (Priority System):** Модели автоматически группируются по мощности (Флагманы $\rightarrow$ Средние $\rightarrow$ Легкие) на основе их ID и названий.
- **Heartbeat-синхронизация:** Система в фоновом режиме обновляет список доступных бесплатных моделей каждые 30 минут.
- **Cyberpunk UI:** Современный интерфейс с эффектом Glassmorphism, неоновыми индикаторами состояния и системой вкладок.
- **Мониторинг:** Отслеживание общего количества успешных запросов, ошибок и скорости генерации (TTOK).

## 🛠 Установка и запуск

### 1. Зависимости
Для работы приложения необходим Python 3.8+ и следующие библиотеки:
```bash
pip install fastapi uvicorn httpx pywebview jinja2

2. Запуск

python main.py

⚙️ Настройка

  1. После запуска откроется окно управления.
  2. Перейдите во вкладку Settings.
  3. Введите ваши API ключи для:
    • OpenRouter
    • OpenCode
    • Kilo AI
  4. Нажмите Apply Configuration.

🔌 Интеграция с IDE/Интерфейсами

Чтобы использовать Cyber Router в вашем приложении (например, в Cursor или Open WebUI), укажите следующие настройки:

  • Base URL: http://127.0.0.1:8000/v1
  • API Key: любой (например, sk-cyber-router)
  • Model: cyber-free

📊 Интерфейс управления

  • Dashboard: Мониторинг доступных моделей. ✅ — Online, ❌ — В кулдауне.
  • Settings: Управление ключами и эндпоинтами.
  • System Stats: Глобальная статистика работы прокси.
  • Amnesty: Возможность вручную вернуть модель из списка забаненных до истечения срока кулдауна.

🛡 Технические детали

  • Кулдаун: При ошибке модель отправляется в «бан» на 3 часа.
  • Приоритеты:
    • PTY 100: Модели уровня R1, DeepSeek-V3, Llama-405B.
    • PTY 50: Модели уровня Llama-8B, Gemma-2.
    • PTY 10: Остальные доступные модели.
      ```
Edit

Pub: 08 Jun 2026 19:45 UTC

Views: 18