mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
feat(ai): add OpenRouter and Ollama Cloud providers (#231)
Co-authored-by: Alex Kenley <Alex.Kenley@threatvectorsecurity.com>
This commit is contained in:
@@ -188,7 +188,7 @@ def try_fallback_endpoint(sess, session_id: str) -> dict | None:
|
||||
Returns {"model": ..., "endpoint_url": ..., "endpoint_name": ...} or None.
|
||||
"""
|
||||
import requests as _req
|
||||
from src.endpoint_resolver import build_chat_url, build_headers, normalize_base
|
||||
from src.endpoint_resolver import build_chat_url, build_headers, build_models_url, normalize_base
|
||||
|
||||
current_url = sess.endpoint_url or ""
|
||||
db = SessionLocal()
|
||||
@@ -205,15 +205,19 @@ def try_fallback_endpoint(sess, session_id: str) -> dict | None:
|
||||
if current_url and base in current_url:
|
||||
continue
|
||||
# Quick ping
|
||||
ping_url = base + "/models"
|
||||
headers = {}
|
||||
if ep.api_key:
|
||||
headers["Authorization"] = f"Bearer {ep.api_key}"
|
||||
ping_url = build_models_url(base)
|
||||
headers = build_headers(ep.api_key, base)
|
||||
try:
|
||||
r = _req.get(ping_url, headers=headers, timeout=5)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
models = [m.get("id") for m in (data.get("data") or []) if m.get("id")]
|
||||
if not models:
|
||||
models = [
|
||||
m.get("name") or m.get("model")
|
||||
for m in (data.get("models") or [])
|
||||
if m.get("name") or m.get("model")
|
||||
]
|
||||
if not models:
|
||||
continue
|
||||
# Found a working endpoint — update session
|
||||
|
||||
@@ -62,14 +62,16 @@ def setup_compare_routes(session_manager: SessionManager):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
from core.database import ModelEndpoint
|
||||
from src.endpoint_resolver import build_headers, normalize_base
|
||||
# Find matching endpoint by URL
|
||||
base = normalize_base(endpoint)
|
||||
ep = db.query(ModelEndpoint).filter(
|
||||
ModelEndpoint.base_url == endpoint.replace('/chat/completions', '')
|
||||
ModelEndpoint.base_url == base
|
||||
).first()
|
||||
if ep and ep.api_key:
|
||||
s = session_manager.sessions.get(sid)
|
||||
if s:
|
||||
s.headers = {"Authorization": f"Bearer {ep.api_key}"}
|
||||
s.headers = build_headers(ep.api_key, ep.base_url)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
+69
-14
@@ -16,12 +16,60 @@ from core.database import SessionLocal, ModelEndpoint, Session as DbSession
|
||||
from core.middleware import require_admin
|
||||
from src.llm_core import _detect_provider, ANTHROPIC_MODELS
|
||||
from src.settings import load_settings as _load_settings, save_settings as _save_settings
|
||||
from src.endpoint_resolver import normalize_base as _normalize_base, build_chat_url, build_headers, _anthropic_api_root
|
||||
from src.endpoint_resolver import normalize_base as _normalize_base, build_chat_url
|
||||
from src.auth_helpers import owner_filter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _anthropic_api_root(base: str) -> str:
|
||||
"""Return Anthropic's API root without duplicating /v1."""
|
||||
base = (base or "").strip().rstrip("/")
|
||||
host = urlparse(base).hostname or ""
|
||||
if host.endswith("anthropic.com") and base.endswith("/v1"):
|
||||
return base[:-3].rstrip("/")
|
||||
return base
|
||||
|
||||
|
||||
def _ollama_api_root(base: str) -> str:
|
||||
"""Return Ollama's native API root without depending on deferred imports."""
|
||||
base = (base or "").strip().rstrip("/")
|
||||
parsed = urlparse(base)
|
||||
host = parsed.hostname or ""
|
||||
path = (parsed.path or "").rstrip("/")
|
||||
if path.endswith("/api"):
|
||||
return base
|
||||
if host.endswith("ollama.com"):
|
||||
root = f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme and parsed.netloc else "https://ollama.com"
|
||||
return root.rstrip("/") + "/api"
|
||||
return base
|
||||
|
||||
|
||||
def _models_url(base: str) -> str:
|
||||
"""Return provider-specific model-list URL for route-local probing."""
|
||||
provider = _detect_provider(base)
|
||||
host = urlparse(base).hostname or ""
|
||||
if provider == "anthropic" or host.endswith("anthropic.com"):
|
||||
return _anthropic_api_root(base) + "/v1/models"
|
||||
if provider == "ollama" or host.endswith("ollama.com"):
|
||||
return _ollama_api_root(base) + "/tags"
|
||||
return base.rstrip("/") + "/models"
|
||||
|
||||
|
||||
def _provider_headers(api_key: Optional[str], base: str) -> Dict[str, str]:
|
||||
"""Build provider auth headers without depending on import-time stubs."""
|
||||
if not api_key:
|
||||
return {}
|
||||
provider = _detect_provider(base)
|
||||
host = urlparse(base).hostname or ""
|
||||
if provider == "anthropic" or host.endswith("anthropic.com"):
|
||||
return {
|
||||
"x-api-key": api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
return {"Authorization": f"Bearer {api_key}"}
|
||||
|
||||
|
||||
# ── Curated model lists per provider ──
|
||||
# For cloud providers that return 100+ models, only show these by default.
|
||||
# A model ID matches if it starts with or equals a curated entry.
|
||||
@@ -87,6 +135,7 @@ _URL_TO_CURATED = {
|
||||
"generativelanguage.googleapis.com": "google",
|
||||
"api.x.ai": "xai",
|
||||
"openrouter.ai": "openrouter",
|
||||
"ollama.com": "ollama",
|
||||
}
|
||||
|
||||
|
||||
@@ -183,9 +232,15 @@ def _probe_single_model(base: str, api_key: str, model_id: str, timeout: int = 1
|
||||
payload = _build_anthropic_payload(model_id, messages, 0.0, 5)
|
||||
if _test_tools:
|
||||
payload["tools"] = [{"name": "test", "description": "Test tool", "input_schema": {"type": "object", "properties": {}}}]
|
||||
elif provider == "ollama":
|
||||
from src.llm_core import _build_ollama_payload
|
||||
target_url = build_chat_url(base)
|
||||
h = _provider_headers(api_key, base)
|
||||
h["Content-Type"] = "application/json"
|
||||
payload = _build_ollama_payload(model_id, messages, 0.0, 5, stream=False, tools=_test_tools)
|
||||
else:
|
||||
target_url = build_chat_url(base)
|
||||
h = build_headers(api_key, base)
|
||||
h = _provider_headers(api_key, base)
|
||||
h["Content-Type"] = "application/json"
|
||||
from src.llm_core import _uses_max_completion_tokens
|
||||
_max_key = "max_completion_tokens" if _uses_max_completion_tokens(model_id) else "max_tokens"
|
||||
@@ -276,10 +331,8 @@ def _probe_endpoint(base_url: str, api_key: str = None, timeout: int = 5) -> Lis
|
||||
return []
|
||||
logger.warning(f"Anthropic /v1/models failed, using hardcoded list: {e}")
|
||||
return list(ANTHROPIC_MODELS)
|
||||
url = base + "/models"
|
||||
headers = {}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
url = _models_url(base)
|
||||
headers = _provider_headers(api_key, base)
|
||||
try:
|
||||
r = httpx.get(url, headers=headers, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
@@ -494,10 +547,7 @@ def setup_model_routes(model_discovery):
|
||||
pass
|
||||
model_ids = [m for m in model_ids if m not in hidden]
|
||||
# Build correct URL based on provider
|
||||
if provider == "anthropic":
|
||||
chat_url = build_chat_url(base)
|
||||
else:
|
||||
chat_url = base + "/chat/completions"
|
||||
chat_url = build_chat_url(base)
|
||||
category = _classify_endpoint(base)
|
||||
|
||||
if model_ids:
|
||||
@@ -671,10 +721,8 @@ def setup_model_routes(model_discovery):
|
||||
entry["error"] = str(e)
|
||||
entry["model_count"] = 0
|
||||
else:
|
||||
url = base + "/models"
|
||||
headers = {}
|
||||
if ep.api_key:
|
||||
headers["Authorization"] = f"Bearer {ep.api_key}"
|
||||
url = _models_url(base)
|
||||
headers = _provider_headers(ep.api_key, base)
|
||||
try:
|
||||
t0 = _time.time()
|
||||
r = httpx.get(url, headers=headers, timeout=5)
|
||||
@@ -682,6 +730,12 @@ def setup_model_routes(model_discovery):
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
models = [m.get("id") for m in (data.get("data") or []) if m.get("id")]
|
||||
if not models:
|
||||
models = [
|
||||
m.get("name") or m.get("model")
|
||||
for m in (data.get("models") or [])
|
||||
if m.get("name") or m.get("model")
|
||||
]
|
||||
entry["status"] = "online"
|
||||
entry["model_count"] = len(models)
|
||||
except Exception as e:
|
||||
@@ -896,6 +950,7 @@ def setup_model_routes(model_discovery):
|
||||
for suffix in ["/models", "/chat/completions", "/completions", "/v1/messages"]:
|
||||
if base_url.endswith(suffix):
|
||||
base_url = base_url[:-len(suffix)].rstrip("/")
|
||||
base_url = _normalize_base(base_url)
|
||||
if not base_url:
|
||||
raise HTTPException(400, "Base URL is required")
|
||||
# Resolve hostname via Tailscale if DNS fails
|
||||
|
||||
@@ -227,6 +227,7 @@ def setup_session_routes(session_manager: SessionManager, config: dict, webhook_
|
||||
)
|
||||
# Set auth headers for custom API-key endpoints
|
||||
resolved_key = api_key.strip() if api_key else ""
|
||||
resolved_base = endpoint_url
|
||||
if not resolved_key and endpoint_id and endpoint_id.strip():
|
||||
from core.database import ModelEndpoint
|
||||
_db = SessionLocal()
|
||||
@@ -234,10 +235,12 @@ def setup_session_routes(session_manager: SessionManager, config: dict, webhook_
|
||||
ep = _db.query(ModelEndpoint).filter(ModelEndpoint.id == endpoint_id.strip()).first()
|
||||
if ep and ep.api_key:
|
||||
resolved_key = ep.api_key
|
||||
resolved_base = ep.base_url
|
||||
finally:
|
||||
_db.close()
|
||||
if resolved_key:
|
||||
session.headers = {"Authorization": f"Bearer {resolved_key}"}
|
||||
from src.endpoint_resolver import build_headers
|
||||
session.headers = build_headers(resolved_key, resolved_base)
|
||||
session_manager.save_sessions()
|
||||
# Fire webhook (sync-safe)
|
||||
if webhook_manager:
|
||||
|
||||
@@ -157,6 +157,7 @@ def setup_webhook_routes(
|
||||
"groq": "https://api.groq.com/openai/v1",
|
||||
"together": "https://api.together.xyz/v1",
|
||||
"openrouter": "https://openrouter.ai/api/v1",
|
||||
"ollama": "https://ollama.com/api",
|
||||
"fireworks": "https://api.fireworks.ai/inference/v1",
|
||||
}
|
||||
|
||||
@@ -203,6 +204,7 @@ def setup_webhook_routes(
|
||||
from core.models import ChatMessage
|
||||
from src.llm_core import llm_call_async
|
||||
from core.database import ModelEndpoint
|
||||
from src.endpoint_resolver import build_chat_url, build_headers, build_models_url, normalize_base
|
||||
|
||||
message = body.message.strip()
|
||||
if not message:
|
||||
@@ -244,7 +246,8 @@ def setup_webhook_routes(
|
||||
"Could not auto-detect provider. Pass base_url (e.g. 'https://api.deepseek.com/v1') "
|
||||
"or provider ('deepseek', 'openai', 'groq', etc.)")
|
||||
|
||||
endpoint_url = base_url + "/chat/completions"
|
||||
base_url = normalize_base(base_url)
|
||||
endpoint_url = build_chat_url(base_url)
|
||||
|
||||
if not session_manager:
|
||||
raise HTTPException(500, "Session manager not available")
|
||||
@@ -254,7 +257,7 @@ def setup_webhook_routes(
|
||||
session_id=sid, name="API Chat", endpoint_url=endpoint_url,
|
||||
model=model, owner=token_owner,
|
||||
)
|
||||
sess.headers = {"Authorization": f"Bearer {api_key}"}
|
||||
sess.headers = build_headers(api_key, base_url)
|
||||
session_manager.save_sessions()
|
||||
session_id = sid
|
||||
|
||||
@@ -271,18 +274,26 @@ def setup_webhook_routes(
|
||||
"No session, api_key, or configured endpoints. "
|
||||
"Pass api_key + model, or configure an endpoint in Admin.")
|
||||
|
||||
endpoint_url = ep.base_url.rstrip("/") + "/chat/completions"
|
||||
base_url = normalize_base(ep.base_url)
|
||||
endpoint_url = build_chat_url(base_url)
|
||||
model = body.model or "auto"
|
||||
api_key = ep.api_key
|
||||
|
||||
if model == "auto":
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
models_url = ep.base_url.rstrip("/") + "/models"
|
||||
hdrs = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
||||
models_url = build_models_url(base_url)
|
||||
hdrs = build_headers(api_key, base_url)
|
||||
resp = await client.get(models_url, headers=hdrs)
|
||||
resp.raise_for_status()
|
||||
ids = [m.get("id") for m in (resp.json().get("data") or []) if m.get("id")]
|
||||
data = resp.json()
|
||||
ids = [m.get("id") for m in (data.get("data") or []) if m.get("id")]
|
||||
if not ids:
|
||||
ids = [
|
||||
m.get("name") or m.get("model")
|
||||
for m in (data.get("models") or [])
|
||||
if m.get("name") or m.get("model")
|
||||
]
|
||||
model = ids[0] if ids else "auto"
|
||||
except Exception:
|
||||
raise HTTPException(500, "Could not discover models from endpoint")
|
||||
@@ -296,7 +307,7 @@ def setup_webhook_routes(
|
||||
model=model, owner=token_owner,
|
||||
)
|
||||
if api_key:
|
||||
sess.headers = {"Authorization": f"Bearer {api_key}"}
|
||||
sess.headers = build_headers(api_key, base_url)
|
||||
session_manager.save_sessions()
|
||||
session_id = sid
|
||||
|
||||
|
||||
Reference in New Issue
Block a user