mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-29 01:02:25 +01:00
fix(security): close bearer auxiliary boundaries
This commit is contained in:
@@ -2821,6 +2821,7 @@ def setup_chat_routes(
|
||||
It just asks the LLM to rewrite the given text.
|
||||
"""
|
||||
require_chat_scope(request)
|
||||
capability = build_request_capability(request)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
@@ -2855,6 +2856,9 @@ def setup_chat_routes(
|
||||
async def stream_rewrite() -> AsyncGenerator[str, None]:
|
||||
full_response = ""
|
||||
try:
|
||||
stream_kwargs = {}
|
||||
if not capability.allow_live_probes:
|
||||
stream_kwargs["allow_live_probes"] = False
|
||||
async for chunk in stream_llm(
|
||||
sess.endpoint_url,
|
||||
sess.model,
|
||||
@@ -2867,6 +2871,7 @@ def setup_chat_routes(
|
||||
# on "Rewriting...". Same fix as the chat max_tokens cap.
|
||||
max_tokens=0,
|
||||
tools=None,
|
||||
**stream_kwargs,
|
||||
):
|
||||
if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"):
|
||||
try:
|
||||
|
||||
@@ -10,10 +10,16 @@ from fastapi import APIRouter, Depends, Request, HTTPException
|
||||
|
||||
from core.models import ChatMessage
|
||||
from core.database import SessionLocal, ChatMessage as DbChatMessage, Session as DbSession
|
||||
from src.auth_helpers import effective_user, is_bearer_principal, require_chat_scope
|
||||
from src.auth_helpers import (
|
||||
effective_user,
|
||||
is_bearer_principal,
|
||||
request_capability,
|
||||
require_chat_scope,
|
||||
)
|
||||
from src.message_metadata import (
|
||||
sanitize_client_message_metadata,
|
||||
sanitize_projected_message_metadata,
|
||||
normalize_client_message_role,
|
||||
)
|
||||
from src.topic_analyzer import analyze_topics
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
@@ -311,7 +317,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
_verify_session_owner(request, session_id)
|
||||
try:
|
||||
body = await request.json()
|
||||
role = body.get("role", "assistant")
|
||||
role = normalize_client_message_role(body.get("role", "assistant"))
|
||||
content = body.get("content", "")
|
||||
if not content:
|
||||
raise HTTPException(400, "content is required")
|
||||
@@ -720,6 +726,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
when the whole chat is approaching compaction.
|
||||
"""
|
||||
require_chat_scope(request)
|
||||
capability = request_capability(request)
|
||||
_verify_session_owner(request, session_id)
|
||||
try:
|
||||
session = session_manager.get_session(session_id)
|
||||
@@ -731,7 +738,14 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
|
||||
messages = session.get_context_messages()
|
||||
used = int(estimate_tokens(messages))
|
||||
ctx_len = int(get_context_length(session.endpoint_url, session.model) or 0)
|
||||
context_kwargs = {}
|
||||
if not capability.allow_live_probes:
|
||||
context_kwargs["allow_live_probes"] = False
|
||||
ctx_len = int(get_context_length(
|
||||
session.endpoint_url,
|
||||
session.model,
|
||||
**context_kwargs,
|
||||
) or 0)
|
||||
pct = round((used / ctx_len) * 100, 1) if ctx_len else 0.0
|
||||
pct = max(0.0, min(100.0, pct))
|
||||
visible_messages = sum(
|
||||
@@ -765,6 +779,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
async def compact_session(request: Request, session_id: str):
|
||||
"""Manually trigger context compaction for a session."""
|
||||
require_chat_scope(request)
|
||||
capability = request_capability(request)
|
||||
_verify_session_owner(request, session_id)
|
||||
from src.auth_helpers import effective_user
|
||||
owner = effective_user(request)
|
||||
@@ -782,7 +797,14 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
if len(session.history) < 6:
|
||||
return {"status": "ok", "message": "Not enough messages to compact"}
|
||||
|
||||
ctx_len = get_context_length(session.endpoint_url, session.model)
|
||||
context_kwargs = {}
|
||||
if not capability.allow_live_probes:
|
||||
context_kwargs["allow_live_probes"] = False
|
||||
ctx_len = get_context_length(
|
||||
session.endpoint_url,
|
||||
session.model,
|
||||
**context_kwargs,
|
||||
)
|
||||
messages_before = session.get_context_messages()
|
||||
used_before = estimate_tokens(messages_before)
|
||||
pct_before = round((used_before / ctx_len) * 100, 1) if ctx_len else 0
|
||||
@@ -809,6 +831,9 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
from src.context_compactor import SELF_SUMMARY_SYSTEM_PROMPT, normalize_compaction_summary
|
||||
compaction_count = sum(1 for m in session.history if isinstance(m, ChatMessage) and "[Conversation summary" in (m.content or ""))
|
||||
sys_prompt = SELF_SUMMARY_SYSTEM_PROMPT.replace("{count}", str(len(older))).replace("{n}", str(compaction_count + 1))
|
||||
compact_kwargs = {}
|
||||
if not capability.allow_live_probes:
|
||||
compact_kwargs["allow_live_probes"] = False
|
||||
summary = await llm_call_async(
|
||||
compact_url, compact_model,
|
||||
[
|
||||
@@ -817,6 +842,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
],
|
||||
temperature=0.2, max_tokens=1024,
|
||||
headers=compact_headers, timeout=30,
|
||||
**compact_kwargs,
|
||||
)
|
||||
summary = normalize_compaction_summary(summary)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import time
|
||||
from services.search import get_search_config, comprehensive_web_search, PROVIDER_INFO
|
||||
from services.search.core import _call_provider
|
||||
from services.search.providers import _get_provider_key, _get_search_instance
|
||||
from src.auth_helpers import require_chat_scope
|
||||
from src.auth_helpers import require_interactive_request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,11 +40,12 @@ async def _request_values(request: Request) -> Dict[str, Any]:
|
||||
def setup_search_routes(config) -> APIRouter:
|
||||
router = APIRouter(
|
||||
tags=["search"],
|
||||
dependencies=[Depends(require_chat_scope)],
|
||||
dependencies=[Depends(require_interactive_request)],
|
||||
)
|
||||
|
||||
@router.get("/api/search/config")
|
||||
async def get_search_settings() -> Dict[str, Any]:
|
||||
async def get_search_settings(request: Request) -> Dict[str, Any]:
|
||||
require_interactive_request(request)
|
||||
return get_search_config()
|
||||
|
||||
@router.post("/api/search")
|
||||
@@ -53,7 +54,7 @@ def setup_search_routes(config) -> APIRouter:
|
||||
|
||||
Used by Compare mode to pre-search once and share results across panes.
|
||||
"""
|
||||
require_chat_scope(request)
|
||||
require_interactive_request(request)
|
||||
values = await _request_values(request)
|
||||
query = str(values.get("query") or values.get("q") or "").strip()
|
||||
if not query:
|
||||
@@ -71,8 +72,9 @@ def setup_search_routes(config) -> APIRouter:
|
||||
return {"context": "", "sources": [], "error": str(e)}
|
||||
|
||||
@router.get("/api/search/providers")
|
||||
async def list_search_providers():
|
||||
async def list_search_providers(request: Request):
|
||||
"""Return available search providers with config status."""
|
||||
require_interactive_request(request)
|
||||
providers = []
|
||||
for pid, (label, needs_key, needs_url) in PROVIDER_INFO.items():
|
||||
if pid == "disabled":
|
||||
@@ -92,7 +94,7 @@ def setup_search_routes(config) -> APIRouter:
|
||||
@router.post("/api/search/query")
|
||||
async def search_with_provider(request: Request) -> Dict[str, Any]:
|
||||
"""Search using a specific provider. Used by compare search mode."""
|
||||
require_chat_scope(request)
|
||||
require_interactive_request(request)
|
||||
values = await _request_values(request)
|
||||
query = str(values.get("query") or values.get("q") or "").strip()
|
||||
provider = str(values.get("provider") or "").strip()
|
||||
|
||||
+84
-51
@@ -16,9 +16,14 @@ from src.auth_helpers import (
|
||||
_auth_disabled,
|
||||
is_bearer_principal,
|
||||
owner_filter,
|
||||
request_capability,
|
||||
require_chat_scope,
|
||||
require_interactive_request,
|
||||
)
|
||||
from src.message_metadata import (
|
||||
normalize_client_message_role,
|
||||
sanitize_client_message_metadata,
|
||||
)
|
||||
from src.message_metadata import sanitize_client_message_metadata
|
||||
from src.session_image_cleanup import _generated_image_path_for_cleanup, session_image_refs
|
||||
from src.session_actions import is_session_recently_active
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
@@ -246,32 +251,37 @@ def setup_session_routes(
|
||||
# session is current and won't delete the live one — this server-side
|
||||
# purge exists only to catch ghosts the frontend missed (tab close,
|
||||
# crash). Only clean up rows old enough to be definitely orphaned.
|
||||
try:
|
||||
from datetime import timedelta as _td
|
||||
_cutoff = utcnow_naive() - _td(minutes=10)
|
||||
_purge_db = SessionLocal()
|
||||
# Listing is an owner-scoped read for bearer integrations. The legacy
|
||||
# incognito cleanup query has no owner predicate and would otherwise
|
||||
# let a chat token mutate another user's stale sessions before the
|
||||
# owner-filtered result is assembled. Browser cleanup remains intact.
|
||||
if not is_bearer_principal(request):
|
||||
try:
|
||||
from core.database import ChatMessage as _DbMsg
|
||||
_ghosts = _purge_db.query(DbSession).filter(
|
||||
DbSession.name.in_(("Nobody", "Incognito")),
|
||||
DbSession.created_at < _cutoff,
|
||||
).all()
|
||||
for _g in _ghosts:
|
||||
if active_incognito_id and _g.id == active_incognito_id:
|
||||
continue
|
||||
_purge_db.query(_DbMsg).filter(_DbMsg.session_id == _g.id).delete()
|
||||
_purge_db.delete(_g)
|
||||
if hasattr(session_manager, "delete_session"):
|
||||
try:
|
||||
session_manager.delete_session(_g.id)
|
||||
except Exception:
|
||||
pass
|
||||
if _ghosts:
|
||||
_purge_db.commit()
|
||||
finally:
|
||||
_purge_db.close()
|
||||
except Exception:
|
||||
pass
|
||||
from datetime import timedelta as _td
|
||||
_cutoff = utcnow_naive() - _td(minutes=10)
|
||||
_purge_db = SessionLocal()
|
||||
try:
|
||||
from core.database import ChatMessage as _DbMsg
|
||||
_ghosts = _purge_db.query(DbSession).filter(
|
||||
DbSession.name.in_(("Nobody", "Incognito")),
|
||||
DbSession.created_at < _cutoff,
|
||||
).all()
|
||||
for _g in _ghosts:
|
||||
if active_incognito_id and _g.id == active_incognito_id:
|
||||
continue
|
||||
_purge_db.query(_DbMsg).filter(_DbMsg.session_id == _g.id).delete()
|
||||
_purge_db.delete(_g)
|
||||
if hasattr(session_manager, "delete_session"):
|
||||
try:
|
||||
session_manager.delete_session(_g.id)
|
||||
except Exception:
|
||||
pass
|
||||
if _ghosts:
|
||||
_purge_db.commit()
|
||||
finally:
|
||||
_purge_db.close()
|
||||
except Exception:
|
||||
pass
|
||||
user_sessions = session_manager.get_sessions_for_user(user)
|
||||
# Fetch folder info from DB for each session
|
||||
db = SessionLocal()
|
||||
@@ -354,6 +364,8 @@ def setup_session_routes(
|
||||
endpoint_id: str = Form(""),
|
||||
):
|
||||
require_chat_scope(request)
|
||||
capability = request_capability(request)
|
||||
probe_kwargs = {} if capability.allow_live_probes else {"allow_live_probes": False}
|
||||
skip_val = str(skip_validation).lower() == "true"
|
||||
user = effective_user(request)
|
||||
endpoint_api_key = ""
|
||||
@@ -405,6 +417,7 @@ def setup_session_routes(
|
||||
headers=validation_headers,
|
||||
owner=user,
|
||||
endpoint_id=endpoint_id.strip() if endpoint_id else None,
|
||||
**probe_kwargs,
|
||||
)
|
||||
if not ids:
|
||||
raise HTTPException(400, "Cannot reach /v1/models")
|
||||
@@ -416,28 +429,35 @@ def setup_session_routes(
|
||||
chat_ids = [m for m in ids if not any(p in m.lower() for p in _NON_CHAT)]
|
||||
model_to_use = (chat_ids or ids)[0]
|
||||
else:
|
||||
from src.llm_core import list_model_ids
|
||||
import os as _os
|
||||
req_base = _os.path.basename(model_to_use.rstrip("/"))
|
||||
avail = list_model_ids(
|
||||
endpoint_url,
|
||||
timeout=SESSION_MODEL_VALIDATION_TIMEOUT,
|
||||
headers=validation_headers,
|
||||
owner=user,
|
||||
endpoint_id=endpoint_id.strip() if endpoint_id else None,
|
||||
)
|
||||
if not avail:
|
||||
raise HTTPException(400, "Cannot reach /v1/models")
|
||||
if model_to_use not in avail:
|
||||
found = None
|
||||
for a in avail:
|
||||
if _os.path.basename(a.rstrip("/")) == req_base:
|
||||
found = a
|
||||
break
|
||||
if not found:
|
||||
raise HTTPException(400,
|
||||
f"Model not found at server. Available: {', '.join(avail)}")
|
||||
model_to_use = found
|
||||
# A bearer with an explicit model is already using an owner-scoped
|
||||
# registered endpoint (raw URLs are rejected above). Do not turn
|
||||
# that synchronous session-creation request into a live catalog
|
||||
# probe merely to validate a value the caller supplied. Interactive
|
||||
# requests retain the existing catalog-backed validation.
|
||||
if capability.allow_live_probes:
|
||||
from src.llm_core import list_model_ids
|
||||
import os as _os
|
||||
req_base = _os.path.basename(model_to_use.rstrip("/"))
|
||||
avail = list_model_ids(
|
||||
endpoint_url,
|
||||
timeout=SESSION_MODEL_VALIDATION_TIMEOUT,
|
||||
headers=validation_headers,
|
||||
owner=user,
|
||||
endpoint_id=endpoint_id.strip() if endpoint_id else None,
|
||||
**probe_kwargs,
|
||||
)
|
||||
if not avail:
|
||||
raise HTTPException(400, "Cannot reach /v1/models")
|
||||
if model_to_use not in avail:
|
||||
found = None
|
||||
for a in avail:
|
||||
if _os.path.basename(a.rstrip("/")) == req_base:
|
||||
found = a
|
||||
break
|
||||
if not found:
|
||||
raise HTTPException(400,
|
||||
f"Model not found at server. Available: {', '.join(avail)}")
|
||||
model_to_use = found
|
||||
|
||||
sid = str(uuid.uuid4())
|
||||
user = effective_user(request)
|
||||
@@ -586,7 +606,7 @@ def setup_session_routes(
|
||||
raise HTTPException(400, "Invalid message attachment metadata") from exc
|
||||
for m in messages:
|
||||
sess.add_message(ChatMessage(
|
||||
m["role"],
|
||||
normalize_client_message_role(m.get("role", "user"), default="user"),
|
||||
m["content"],
|
||||
metadata=sanitize_client_message_metadata(m.get("metadata")),
|
||||
))
|
||||
@@ -1002,6 +1022,7 @@ def setup_session_routes(
|
||||
async def compact_session(request: Request, session_id: str):
|
||||
"""Summarize older messages into one compacted history entry."""
|
||||
require_chat_scope(request)
|
||||
capability = request_capability(request)
|
||||
_verify_session_owner(request, session_id)
|
||||
try:
|
||||
session = session_manager.get_session(session_id)
|
||||
@@ -1046,6 +1067,9 @@ def setup_session_routes(
|
||||
for m in older
|
||||
)
|
||||
try:
|
||||
compact_kwargs = {}
|
||||
if not capability.allow_live_probes:
|
||||
compact_kwargs["allow_live_probes"] = False
|
||||
summary = await llm_call_async(
|
||||
url,
|
||||
model,
|
||||
@@ -1054,6 +1078,7 @@ def setup_session_routes(
|
||||
max_tokens=1024,
|
||||
headers=headers,
|
||||
timeout=60,
|
||||
**compact_kwargs,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Manual compaction failed: %s", e)
|
||||
@@ -1079,7 +1104,10 @@ def setup_session_routes(
|
||||
"message_count": len(new_history),
|
||||
}
|
||||
|
||||
@router.post("/sessions/auto-sort")
|
||||
@router.post(
|
||||
"/sessions/auto-sort",
|
||||
dependencies=[Depends(require_interactive_request)],
|
||||
)
|
||||
def auto_sort_sessions(request: Request, skip_llm: bool = False):
|
||||
"""Use AI to categorize all sessions into folders.
|
||||
|
||||
@@ -1089,6 +1117,7 @@ def setup_session_routes(
|
||||
users can clean junk without spending tokens.
|
||||
"""
|
||||
require_chat_scope(request)
|
||||
require_interactive_request(request)
|
||||
from src.llm_core import llm_call
|
||||
user = effective_user(request)
|
||||
single_user_mode = not user and _auth_disabled()
|
||||
@@ -1370,6 +1399,7 @@ def setup_session_routes(
|
||||
async def get_context_info(request: Request, session_id: str):
|
||||
"""Get the real context length for a session's model from the endpoint."""
|
||||
require_chat_scope(request)
|
||||
capability = request_capability(request)
|
||||
_verify_session_owner(request, session_id)
|
||||
session = session_manager.get_session(session_id)
|
||||
if not session:
|
||||
@@ -1378,7 +1408,10 @@ def setup_session_routes(
|
||||
return {"context_length": None}
|
||||
try:
|
||||
from src.model_context import get_context_length
|
||||
ctx = get_context_length(session.endpoint_url, session.model)
|
||||
context_kwargs = {}
|
||||
if not capability.allow_live_probes:
|
||||
context_kwargs["allow_live_probes"] = False
|
||||
ctx = get_context_length(session.endpoint_url, session.model, **context_kwargs)
|
||||
return {"context_length": ctx, "model": session.model}
|
||||
except Exception:
|
||||
return {"context_length": None}
|
||||
|
||||
+14
-2
@@ -21,7 +21,12 @@ from core.database import (
|
||||
Note,
|
||||
Session as DbSession,
|
||||
)
|
||||
from src.auth_helpers import effective_user, require_chat_scope, require_non_bearer_request
|
||||
from src.auth_helpers import (
|
||||
effective_user,
|
||||
is_bearer_principal,
|
||||
require_chat_scope,
|
||||
require_non_bearer_request,
|
||||
)
|
||||
from src.attachment_refs import attachment_refs_from_metadata
|
||||
from src.constants import GENERATED_IMAGES_DIR
|
||||
from src.upload_handler import (
|
||||
@@ -379,7 +384,14 @@ def setup_upload_routes(upload_handler):
|
||||
auth_configured = bool(auth_mgr and auth_mgr.is_configured)
|
||||
current_user = effective_user(request)
|
||||
file_owner = info.get("owner") if info else None
|
||||
if auth_configured:
|
||||
if is_bearer_principal(request):
|
||||
# A token owner is an owner-bound data principal, even when that
|
||||
# owner is an administrator. Do not reuse the browser admin
|
||||
# fallback for bearer downloads or an admin token can read another
|
||||
# user's upload by ID.
|
||||
if not current_user or file_owner != current_user:
|
||||
raise HTTPException(404, "File not found")
|
||||
elif auth_configured:
|
||||
if not current_user:
|
||||
raise HTTPException(403, "Access denied")
|
||||
if file_owner != current_user and not auth_mgr.is_admin(current_user):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
@@ -9,7 +10,12 @@ from fastapi import APIRouter, HTTPException, Request, Form
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.database import SessionLocal, Webhook, ModelEndpoint
|
||||
from src.auth_helpers import is_bearer_principal, owner_filter, require_chat_scope
|
||||
from src.auth_helpers import (
|
||||
is_bearer_principal,
|
||||
owner_filter,
|
||||
request_capability,
|
||||
require_chat_scope,
|
||||
)
|
||||
from src.url_security import validate_public_http_url
|
||||
from src.webhook_manager import WebhookManager, validate_webhook_url, validate_events
|
||||
|
||||
@@ -62,6 +68,35 @@ def _caller_owns_session(sess_owner, caller) -> bool:
|
||||
return sess_owner == caller
|
||||
|
||||
|
||||
def _cached_endpoint_model_ids(endpoint) -> list[str]:
|
||||
"""Return model IDs already stored for a configured endpoint.
|
||||
|
||||
The synchronous bearer integration may use a cached model or the provider's
|
||||
``auto`` alias, but it must not turn an ordinary chat request into a remote
|
||||
catalog probe. Malformed/legacy cache shapes are treated as empty.
|
||||
"""
|
||||
raw = getattr(endpoint, "cached_models", None)
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
value = json.loads(raw) if isinstance(raw, str) else raw
|
||||
except (TypeError, ValueError):
|
||||
return []
|
||||
if isinstance(value, dict):
|
||||
value = value.get("data") or value.get("models") or []
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
ids = []
|
||||
for item in value:
|
||||
if isinstance(item, str) and item.strip():
|
||||
ids.append(item.strip())
|
||||
elif isinstance(item, dict):
|
||||
model_id = item.get("id") or item.get("name") or item.get("model")
|
||||
if isinstance(model_id, str) and model_id.strip():
|
||||
ids.append(model_id.strip())
|
||||
return ids
|
||||
|
||||
|
||||
def setup_webhook_routes(
|
||||
webhook_manager: WebhookManager,
|
||||
auth_manager,
|
||||
@@ -240,12 +275,13 @@ def setup_webhook_routes(
|
||||
if getattr(request.state, "api_token", False) is not True:
|
||||
raise HTTPException(403, "This endpoint requires an API token")
|
||||
token_owner = require_chat_scope(request)
|
||||
capability = request_capability(request)
|
||||
if not token_owner:
|
||||
raise HTTPException(403, "API token has no owner")
|
||||
|
||||
from core.models import ChatMessage
|
||||
from src.llm_core import llm_call_async
|
||||
from src.endpoint_resolver import build_chat_url, build_headers, build_models_url, normalize_base
|
||||
from src.endpoint_resolver import build_chat_url, build_headers, normalize_base
|
||||
|
||||
message = body.message.strip()
|
||||
if not message:
|
||||
@@ -337,28 +373,12 @@ def setup_webhook_routes(
|
||||
raise HTTPException(500, "Could not resolve endpoint credentials")
|
||||
|
||||
if model == "auto":
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
models_url = build_models_url(base_url)
|
||||
hdrs = build_headers(api_key, base_url)
|
||||
if models_url:
|
||||
resp = await client.get(models_url, headers=hdrs)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
items = data if isinstance(data, list) else (data.get("data") or [])
|
||||
ids = [m.get("id") for m in items if isinstance(m, dict) and m.get("id")]
|
||||
if not ids and isinstance(data, dict):
|
||||
ids = [
|
||||
m.get("name") or m.get("model")
|
||||
for m in (data.get("models") or [])
|
||||
if m.get("name") or m.get("model")
|
||||
]
|
||||
else:
|
||||
import json as _json
|
||||
ids = _json.loads(ep.cached_models or "[]")
|
||||
model = ids[0] if ids else "auto"
|
||||
except Exception:
|
||||
raise HTTPException(500, "Could not discover models from endpoint")
|
||||
# This route is bearer-only. Resolve auto from the endpoint's
|
||||
# already persisted catalog and leave the provider alias in
|
||||
# place when no cache exists; neither choice needs a new
|
||||
# /models or /tags request during ordinary chat.
|
||||
ids = _cached_endpoint_model_ids(ep)
|
||||
model = ids[0] if ids else "auto"
|
||||
|
||||
if not session_manager:
|
||||
raise HTTPException(500, "Session manager not available")
|
||||
@@ -378,9 +398,13 @@ def setup_webhook_routes(
|
||||
|
||||
messages = [{"role": m.role, "content": m.content} for m in sess.history]
|
||||
|
||||
llm_kwargs = {}
|
||||
if not capability.allow_live_probes:
|
||||
llm_kwargs["allow_live_probes"] = False
|
||||
reply = await llm_call_async(
|
||||
sess.endpoint_url, sess.model, messages,
|
||||
headers=sess.headers, timeout=120,
|
||||
**llm_kwargs,
|
||||
)
|
||||
sess.add_message(ChatMessage("assistant", reply))
|
||||
session_manager.save_sessions()
|
||||
|
||||
Reference in New Issue
Block a user