Ollama Cloud provider + model browser + OpenRouter key fix
New provider: Ollama Cloud (ollama.com) - Native Ollama chat API with bearer token auth - Provider card in Admin → Providers tab - "Ollama Cloud" tab with Pull Models button (fetches 36 models) - Search/filter models, one-click Add - Models route as ollama_cloud::modelname through query_ollama_cloud() - Test button verifies connection OpenRouter fix: - Cleared bad API key from config (was dd00bea4... not sk-or-) - Real key from /home/profit/.env now used (sk-or-v1-579...) - Fixed OpenAI provider that had wrong base_url (ollama.com→api.openai.com) - Bumped OR timeout to 180s for free model rate limits Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e5e17a71a7
commit
fa6ccff079
139
llm_team_ui.py
139
llm_team_ui.py
@ -1842,6 +1842,7 @@ DEFAULT_CONFIG = {
|
|||||||
"providers": {
|
"providers": {
|
||||||
"ollama": {"enabled": True, "base_url": "http://localhost:11434", "timeout": 300},
|
"ollama": {"enabled": True, "base_url": "http://localhost:11434", "timeout": 300},
|
||||||
"openrouter": {"enabled": False, "base_url": "https://openrouter.ai/api/v1", "api_key": "", "timeout": 120},
|
"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},
|
"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},
|
"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", "")
|
key = prov.get("api_key", "")
|
||||||
if key:
|
if key:
|
||||||
return 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, ""), "")
|
return os.environ.get(env_map.get(provider_name, ""), "")
|
||||||
|
|
||||||
DB_DSN = "dbname=knowledge_base user=kbuser password=IPbLBA0EQI8u4TeM2YZrbm1OAy5nSwqC host=localhost"
|
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; }
|
.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 { 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 { 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.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.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); }
|
.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"""
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" onclick="switchTab('providers')">Providers</div>
|
<div class="tab active" onclick="switchTab('providers')">Providers</div>
|
||||||
<div class="tab" onclick="switchTab('models')">Models</div>
|
<div class="tab" onclick="switchTab('models')">Models</div>
|
||||||
|
<div class="tab" onclick="switchTab('ollama_cloud')">Ollama Cloud</div>
|
||||||
<div class="tab" onclick="switchTab('openrouter')">OpenRouter</div>
|
<div class="tab" onclick="switchTab('openrouter')">OpenRouter</div>
|
||||||
<div class="tab" onclick="switchTab('timeouts')">Timeouts</div>
|
<div class="tab" onclick="switchTab('timeouts')">Timeouts</div>
|
||||||
<div class="tab" onclick="switchTab('security')">Security</div>
|
<div class="tab" onclick="switchTab('security')">Security</div>
|
||||||
@ -4970,6 +4973,15 @@ ADMIN_HTML = r"""
|
|||||||
<div class="row"><label>Timeout (s)</label><input id="ollama-timeout" type="number" value="300" style="width:80px;flex:none" onchange="updateProvider('ollama')">
|
<div class="row"><label>Timeout (s)</label><input id="ollama-timeout" type="number" value="300" style="width:80px;flex:none" onchange="updateProvider('ollama')">
|
||||||
<button class="btn" onclick="testProvider('ollama')">Test</button></div>
|
<button class="btn" onclick="testProvider('ollama')">Test</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card" id="prov-ollama_cloud">
|
||||||
|
<h3><div class="prov-dot" style="background:var(--accent2)"></div> Ollama Cloud
|
||||||
|
<label class="toggle" style="margin-left:auto"><input type="checkbox" id="ollama_cloud-enabled" onchange="updateProvider('ollama_cloud')"><span class="slider"></span></label></h3>
|
||||||
|
<div class="row"><label>API Key</label><input id="ollama_cloud-key" type="password" placeholder="Ollama API key" onchange="updateProvider('ollama_cloud')">
|
||||||
|
<button class="btn btn-sm" onclick="toggleVis('ollama_cloud-key')">Show</button></div>
|
||||||
|
<div class="row"><label>Base URL</label><input id="ollama_cloud-url" value="https://ollama.com" onchange="updateProvider('ollama_cloud')"></div>
|
||||||
|
<div class="row"><label>Timeout (s)</label><input id="ollama_cloud-timeout" type="number" value="180" style="width:80px;flex:none" onchange="updateProvider('ollama_cloud')">
|
||||||
|
<button class="btn" onclick="testProvider('ollama_cloud')">Test</button></div>
|
||||||
|
</div>
|
||||||
<div class="card" id="prov-openrouter">
|
<div class="card" id="prov-openrouter">
|
||||||
<h3><div class="prov-dot" style="background:var(--blue)"></div> OpenRouter
|
<h3><div class="prov-dot" style="background:var(--blue)"></div> OpenRouter
|
||||||
<label class="toggle" style="margin-left:auto"><input type="checkbox" id="openrouter-enabled" onchange="updateProvider('openrouter')"><span class="slider"></span></label></h3>
|
<label class="toggle" style="margin-left:auto"><input type="checkbox" id="openrouter-enabled" onchange="updateProvider('openrouter')"><span class="slider"></span></label></h3>
|
||||||
@ -5016,7 +5028,7 @@ ADMIN_HTML = r"""
|
|||||||
</div>
|
</div>
|
||||||
<div id="add-cloud-modal" class="card" style="display:none;border-color:var(--accent)">
|
<div id="add-cloud-modal" class="card" style="display:none;border-color:var(--accent)">
|
||||||
<h3>Add Cloud Model</h3>
|
<h3>Add Cloud Model</h3>
|
||||||
<div class="row"><label>Provider</label><select id="add-cloud-prov"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option></select></div>
|
<div class="row"><label>Provider</label><select id="add-cloud-prov"><option value="openrouter">OpenRouter</option><option value="ollama_cloud">Ollama Cloud</option><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option></select></div>
|
||||||
<div class="row"><label>Model ID</label><input id="add-cloud-id" placeholder="e.g. meta-llama/llama-3-8b-instruct:free"></div>
|
<div class="row"><label>Model ID</label><input id="add-cloud-id" placeholder="e.g. meta-llama/llama-3-8b-instruct:free"></div>
|
||||||
<div class="row"><label>Display Name</label><input id="add-cloud-name" placeholder="e.g. Llama 3 8B Free"></div>
|
<div class="row"><label>Display Name</label><input id="add-cloud-name" placeholder="e.g. Llama 3 8B Free"></div>
|
||||||
<div class="row" style="justify-content:flex-end;gap:6px">
|
<div class="row" style="justify-content:flex-end;gap:6px">
|
||||||
@ -5026,6 +5038,15 @@ ADMIN_HTML = r"""
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OLLAMA CLOUD TAB -->
|
||||||
|
<div id="tab-ollama_cloud" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<h3>Models on Ollama Cloud <button class="btn btn-primary" style="margin-left:auto" onclick="fetchOCModels()">Pull Models</button></h3>
|
||||||
|
<input class="search-input" id="oc-search" placeholder="Search models..." oninput="filterOC()">
|
||||||
|
<div class="or-list" id="oc-model-list"><div class="empty">Click "Pull Models" to load available models from ollama.com</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- OPENROUTER TAB -->
|
<!-- OPENROUTER TAB -->
|
||||||
<div id="tab-openrouter" class="tab-content">
|
<div id="tab-openrouter" class="tab-content">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -5328,6 +5349,62 @@ async function addOR(id, name) {
|
|||||||
toast('Added: ' + 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() {
|
async function saveTimeouts() {
|
||||||
var g = parseInt(document.getElementById('global-timeout').value) || 300;
|
var g = parseInt(document.getElementById('global-timeout').value) || 300;
|
||||||
config.timeouts = config.timeouts || {};
|
config.timeouts = config.timeouts || {};
|
||||||
@ -6493,6 +6570,26 @@ def query_ollama(model, prompt, timeout):
|
|||||||
return resp.json()["response"]
|
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 ─────────────────────────
|
# ─── MODEL RATE-LIMIT TIMEOUT SYSTEM ─────────────────────────
|
||||||
# Models that get 429'd are auto-disabled until admin re-enables them.
|
# Models that get 429'd are auto-disabled until admin re-enables them.
|
||||||
_model_rate_limited = {} # model_id -> {"since": timestamp, "reason": str, "count": int}
|
_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)
|
provider_name, model_name = model_id.split("::", 1)
|
||||||
if provider_name == "anthropic":
|
if provider_name == "anthropic":
|
||||||
return query_anthropic(model_name, prompt, timeout)
|
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_openai_compatible(model_name, prompt, provider_name, timeout)
|
||||||
return query_ollama(model_id, prompt, timeout)
|
return query_ollama(model_id, prompt, timeout)
|
||||||
except requests.exceptions.HTTPError as e:
|
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)
|
r = requests.get(f"{prov.get('base_url', 'http://localhost:11434')}/api/tags", timeout=5)
|
||||||
count = len(r.json().get("models", []))
|
count = len(r.json().get("models", []))
|
||||||
return jsonify({"ok": True, "message": f"Connected. {count} models available."})
|
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":
|
elif name == "openrouter":
|
||||||
key = data.get("api_key") or get_api_key("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",
|
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)})
|
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")
|
@app.route("/api/admin/ollama-models")
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin_ollama_models():
|
def admin_ollama_models():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user