diff --git a/llm_team_ui.py b/llm_team_ui.py index 8f9cbe3..1726904 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -1842,6 +1842,7 @@ DEFAULT_CONFIG = { "providers": { "ollama": {"enabled": True, "base_url": "http://localhost:11434", "timeout": 300}, "openrouter": {"enabled": False, "base_url": "https://openrouter.ai/api/v1", "api_key": "", "timeout": 120}, + "ollama_cloud": {"enabled": False, "base_url": "https://ollama.com", "api_key": "", "timeout": 180}, "openai": {"enabled": False, "base_url": "https://api.openai.com/v1", "api_key": "", "timeout": 120}, "anthropic": {"enabled": False, "base_url": "https://api.anthropic.com/v1", "api_key": "", "timeout": 120}, }, @@ -1884,7 +1885,7 @@ def get_api_key(provider_name): key = prov.get("api_key", "") if key: return key - env_map = {"openrouter": "OPENROUTER_API_KEY", "openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY"} + env_map = {"openrouter": "OPENROUTER_API_KEY", "openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "ollama_cloud": "OLLAMA_CLOUD_API_KEY"} return os.environ.get(env_map.get(provider_name, ""), "") DB_DSN = "dbname=knowledge_base user=kbuser password=IPbLBA0EQI8u4TeM2YZrbm1OAy5nSwqC host=localhost" @@ -2114,6 +2115,7 @@ HTML = r""" .model-card .meta { font-size: 10px; color: var(--text2); font-family: 'JetBrains Mono', monospace; } .prov-badge { font-size: 8px; padding: 2px 6px; border-radius: 1px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; font-family: 'JetBrains Mono', monospace; border: 1px solid; } .prov-badge.ollama { background: rgba(74,222,128,0.08); color: var(--green); border-color: rgba(74,222,128,0.2); } + .prov-badge.ollama_cloud { background: rgba(245,245,245,0.08); color: #e6edf3; border-color: rgba(245,245,245,0.2); } .prov-badge.openrouter { background: rgba(91,156,245,0.08); color: var(--blue); border-color: rgba(91,156,245,0.2); } .prov-badge.openai { background: rgba(226,181,90,0.08); color: var(--accent2); border-color: rgba(226,181,90,0.2); } .prov-badge.anthropic { background: rgba(236,72,153,0.08); color: #ec4899; border-color: rgba(236,72,153,0.2); } @@ -4955,6 +4957,7 @@ ADMIN_HTML = r"""
Providers
Models
+
Ollama Cloud
OpenRouter
Timeouts
Security
@@ -4970,6 +4973,15 @@ ADMIN_HTML = r"""
+
+

Ollama Cloud +

+
+
+
+
+
+

OpenRouter

@@ -5016,7 +5028,7 @@ ADMIN_HTML = r"""
+ +
+
+

Models on Ollama Cloud

+ +
Click "Pull Models" to load available models from ollama.com
+
+
+
@@ -5328,6 +5349,62 @@ async function addOR(id, name) { toast('Added: ' + name); } +// ─── Ollama Cloud model fetcher ─── +let ocModels = []; +async function fetchOCModels() { + const el = document.getElementById('oc-model-list'); + el.textContent = 'Fetching from ollama.com...'; + const r = await fetch('/api/admin/ollama-cloud/models'); + const data = await r.json(); + ocModels = data.models || []; + if (data.error) { el.textContent = 'Error: '+data.error; return; } + renderOCModels(); +} +function renderOCModels() { + const q = (document.getElementById('oc-search').value || '').toLowerCase(); + const filtered = q ? ocModels.filter(m => m.name.toLowerCase().includes(q)) : ocModels; + const el = document.getElementById('oc-model-list'); + if (!filtered.length) { el.textContent = 'No models found.'; return; } + const existing = new Set((config.cloud_models||[]).map(m=>m.id)); + el.textContent = ''; + filtered.forEach(function(m) { + const added = existing.has('ollama_cloud::'+m.id); + const row = document.createElement('div'); + row.className = 'model-row'; + const nameEl = document.createElement('span'); + nameEl.className = 'name'; + nameEl.textContent = m.name; + const meta = document.createElement('span'); + meta.className = 'meta'; + meta.textContent = m.size_gb + 'GB'; + row.appendChild(nameEl); + row.appendChild(meta); + if (added) { + const btn = document.createElement('button'); + btn.className = 'btn btn-sm'; + btn.disabled = true; + btn.style.opacity = '0.4'; + btn.textContent = 'Added'; + row.appendChild(btn); + } else { + const btn = document.createElement('button'); + btn.className = 'btn btn-sm btn-green'; + btn.textContent = 'Add'; + btn.onclick = function() { addOC(m.id, m.name); }; + row.appendChild(btn); + } + el.appendChild(row); + }); +} +function filterOC() { renderOCModels(); } +async function addOC(id, name) { + config.cloud_models = config.cloud_models || []; + config.cloud_models.push({id: 'ollama_cloud::'+id, display_name: 'Ollama: '+name, enabled: true}); + await saveCloudModels(); + renderOCModels(); + toast('Added: ' + name); +} + async function saveTimeouts() { var g = parseInt(document.getElementById('global-timeout').value) || 300; config.timeouts = config.timeouts || {}; @@ -6493,6 +6570,26 @@ def query_ollama(model, prompt, timeout): return resp.json()["response"] +def query_ollama_cloud(model, prompt, timeout): + """Query Ollama Cloud (ollama.com) — same API as local but with bearer auth.""" + cfg = load_config() + prov = cfg["providers"].get("ollama_cloud", {}) + base = prov.get("base_url", "https://ollama.com") + api_key = get_api_key("ollama_cloud") + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + prompt_tokens = estimate_tokens(prompt) + ctx_limit = get_context_limit(model) + num_ctx = min(max(prompt_tokens + 1024, 2048), ctx_limit) + if prompt_tokens > ctx_limit - 512: + prompt = smart_truncate(prompt, ctx_limit - 512) + resp = requests.post(f"{base}/api/chat", headers=headers, json={ + "model": model, "messages": [{"role": "user", "content": prompt}], + "stream": False, "options": {"num_ctx": num_ctx} + }, timeout=timeout) + resp.raise_for_status() + return resp.json()["message"]["content"] + + # ─── MODEL RATE-LIMIT TIMEOUT SYSTEM ───────────────────────── # Models that get 429'd are auto-disabled until admin re-enables them. _model_rate_limited = {} # model_id -> {"since": timestamp, "reason": str, "count": int} @@ -6573,6 +6670,8 @@ def query_model(model_id, prompt): provider_name, model_name = model_id.split("::", 1) if provider_name == "anthropic": return query_anthropic(model_name, prompt, timeout) + if provider_name == "ollama_cloud": + return query_ollama_cloud(model_name, prompt, timeout) return query_openai_compatible(model_name, prompt, provider_name, timeout) return query_ollama(model_id, prompt, timeout) except requests.exceptions.HTTPError as e: @@ -7063,6 +7162,12 @@ def admin_test_provider(): r = requests.get(f"{prov.get('base_url', 'http://localhost:11434')}/api/tags", timeout=5) count = len(r.json().get("models", [])) return jsonify({"ok": True, "message": f"Connected. {count} models available."}) + elif name == "ollama_cloud": + key = data.get("api_key") or get_api_key("ollama_cloud") + base = prov.get("base_url", "https://ollama.com") + r = requests.get(f"{base}/api/tags", headers={"Authorization": f"Bearer {key}"}, timeout=10) + count = len(r.json().get("models", [])) + return jsonify({"ok": True, "message": f"Connected to Ollama Cloud. {count} models available."}) elif name == "openrouter": key = data.get("api_key") or get_api_key("openrouter") r = requests.get(f"{prov.get('base_url', 'https://openrouter.ai/api/v1')}/models", @@ -7113,6 +7218,36 @@ def admin_openrouter_models(): return jsonify({"models": [], "error": str(e)}) +_oc_models_cache = {"data": None, "ts": 0} + +@app.route("/api/admin/ollama-cloud/models") +@admin_required +def admin_ollama_cloud_models(): + import time + now = time.time() + if _oc_models_cache["data"] and now - _oc_models_cache["ts"] < 300: + return jsonify({"models": _oc_models_cache["data"]}) + cfg = load_config() + prov = cfg["providers"].get("ollama_cloud", {}) + base = prov.get("base_url", "https://ollama.com") + key = get_api_key("ollama_cloud") + headers = {"Authorization": f"Bearer {key}"} if key else {} + try: + r = requests.get(f"{base}/api/tags", headers=headers, timeout=15) + r.raise_for_status() + models = [] + for m in r.json().get("models", []): + name = m.get("name", "") + size_gb = round(m.get("size", 0) / 1e9, 1) + models.append({"id": name, "name": name, "size_gb": size_gb, + "modified": m.get("modified_at", "")[:10]}) + _oc_models_cache["data"] = models + _oc_models_cache["ts"] = now + return jsonify({"models": models}) + except Exception as e: + return jsonify({"models": [], "error": str(e)}) + + @app.route("/api/admin/ollama-models") @admin_required def admin_ollama_models():