mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-29 01:02:25 +01:00
Compare commits
2
Commits
5ce2056521
...
4e46e415ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e46e415ea | ||
|
|
6a2a39f892 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
# src/exceptions.py
|
||||
# core/exceptions.py
|
||||
"""Custom exceptions for the application."""
|
||||
|
||||
class SessionNotFoundError(Exception):
|
||||
|
||||
+19
-26
@@ -1,29 +1,22 @@
|
||||
# src/exceptions.py
|
||||
"""Custom exceptions for the application."""
|
||||
"""Backward-compatible shim — the single source of truth is core/exceptions.py.
|
||||
|
||||
class SessionNotFoundError(Exception):
|
||||
"""Raised when a requested session is not found."""
|
||||
def __init__(self, session_id: str):
|
||||
self.session_id = session_id
|
||||
super().__init__(f"Session '{session_id}' not found")
|
||||
Historically this module was a byte-for-byte duplicate of core/exceptions.py,
|
||||
which is the canonical definition (imported by app.py, core/__init__.py, and
|
||||
routes/chat_routes.py). To kill the drift, this now simply re-exports the
|
||||
exception classes from core.exceptions so there is exactly one place that
|
||||
defines them. Existing `from src.exceptions import ...` callers keep working.
|
||||
"""
|
||||
from core.exceptions import ( # noqa: F401
|
||||
SessionNotFoundError,
|
||||
InvalidFileUploadError,
|
||||
LLMServiceError,
|
||||
WebSearchError,
|
||||
)
|
||||
|
||||
class InvalidFileUploadError(Exception):
|
||||
"""Raised when a file upload fails validation."""
|
||||
def __init__(self, message: str, filename: str = None):
|
||||
self.filename = filename
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
class LLMServiceError(Exception):
|
||||
"""Raised when there is an error communicating with the LLM service."""
|
||||
def __init__(self, message: str, endpoint: str = None):
|
||||
self.endpoint = endpoint
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
class WebSearchError(Exception):
|
||||
"""Raised when there is an error with web search functionality."""
|
||||
def __init__(self, message: str, query: str = None):
|
||||
self.query = query
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
__all__ = [
|
||||
"SessionNotFoundError",
|
||||
"InvalidFileUploadError",
|
||||
"LLMServiceError",
|
||||
"WebSearchError",
|
||||
]
|
||||
|
||||
@@ -289,6 +289,42 @@ def _checkin_calendar_events(db, owner, start, end):
|
||||
)
|
||||
|
||||
|
||||
def _normalize_chat_endpoint(url: str) -> str:
|
||||
"""Repair a resolved task endpoint to a full chat-completions URL.
|
||||
|
||||
Unlike the chat path — which stores ``build_chat_url(normalize_base(base))``
|
||||
on the session — the task executor passes ``task.endpoint_url`` verbatim to
|
||||
the model HTTP call. A bare OpenAI-compatible base such as
|
||||
``http://host:11434/v1`` therefore POSTs to a 404 ("page not found") and the
|
||||
model silently appears to "return an empty response".
|
||||
|
||||
Repair only bare OpenAI-compatible bases. Native-Ollama URLs (``/api...``)
|
||||
and URLs that already point at a concrete endpoint are returned untouched, so
|
||||
their own downstream normalizers keep working. Idempotent: a URL already
|
||||
ending in ``/chat/completions`` is left as-is.
|
||||
"""
|
||||
if not url:
|
||||
return url
|
||||
# Imports kept function-local (endpoint_resolver pulls in heavy deps) but
|
||||
# OUTSIDE the try: an import failure is a real bug that should surface, not
|
||||
# be silently swallowed into the un-normalized URL this function exists to
|
||||
# repair.
|
||||
from urllib.parse import urlparse
|
||||
from src.endpoint_resolver import normalize_base, build_chat_url
|
||||
path = (urlparse(url).path or "").rstrip("/")
|
||||
if path == "/api" or path.startswith("/api/"):
|
||||
return url # native Ollama — handled by the native path downstream
|
||||
if path.endswith(("/chat/completions", "/messages", "/responses", "/completions")):
|
||||
return url # already a concrete endpoint
|
||||
try:
|
||||
return build_chat_url(normalize_base(url))
|
||||
except Exception:
|
||||
# Guard only the actual normalization. Returning the URL un-normalized
|
||||
# reverts to the 404 this fixes, so make the silent revert visible.
|
||||
logger.debug("task endpoint normalization failed for %r; using as-is", url, exc_info=True)
|
||||
return url
|
||||
|
||||
|
||||
class TaskScheduler:
|
||||
def __init__(self, session_manager):
|
||||
self._session_manager = session_manager
|
||||
@@ -1357,6 +1393,7 @@ class TaskScheduler:
|
||||
endpoint_url, model = self._resolve_defaults(db, task.owner)
|
||||
if not endpoint_url or not model:
|
||||
raise RuntimeError("No model/endpoint configured")
|
||||
endpoint_url = _normalize_chat_endpoint(endpoint_url)
|
||||
# Record the resolved model so _execute_task_locked can persist it on
|
||||
# the run (tasks rarely pin a model, so this is the only record of
|
||||
# which model actually produced the output).
|
||||
@@ -1548,6 +1585,8 @@ class TaskScheduler:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
endpoint_url = _normalize_chat_endpoint(endpoint_url)
|
||||
|
||||
session_id = task.session_id
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
@@ -1821,6 +1860,7 @@ class TaskScheduler:
|
||||
endpoint_url, model = self._resolve_defaults(db, task.owner)
|
||||
if not endpoint_url or not model:
|
||||
raise RuntimeError("No model/endpoint configured for research")
|
||||
endpoint_url = _normalize_chat_endpoint(endpoint_url)
|
||||
# Record the resolved model for the run record (see _execute_task_locked).
|
||||
self._last_run_model = model
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Regression test for the task-path endpoint-URL normalization fix.
|
||||
|
||||
Bug: the task executor passed ``task.endpoint_url`` verbatim to the model HTTP
|
||||
call (unlike the chat path, which normalizes via ``build_chat_url``). A bare
|
||||
OpenAI-compatible base such as ``http://host:11434/v1`` POSTed to a 404 and the
|
||||
run silently reported "The model returned an empty response".
|
||||
|
||||
The fix routes every resolved task endpoint through ``_normalize_chat_endpoint``.
|
||||
"""
|
||||
from src.task_scheduler import _normalize_chat_endpoint
|
||||
|
||||
|
||||
def test_bare_v1_base_gets_chat_completions_suffix():
|
||||
# The exact failure case: a bare /v1 base must become a full chat URL.
|
||||
assert (
|
||||
_normalize_chat_endpoint("http://localhost:11434/v1")
|
||||
== "http://localhost:11434/v1/chat/completions"
|
||||
)
|
||||
|
||||
|
||||
def test_full_chat_url_is_unchanged_idempotent():
|
||||
full = "http://localhost:11434/v1/chat/completions"
|
||||
assert _normalize_chat_endpoint(full) == full
|
||||
# Idempotent under repeated application.
|
||||
assert _normalize_chat_endpoint(_normalize_chat_endpoint(full)) == full
|
||||
|
||||
|
||||
def test_native_ollama_url_left_alone():
|
||||
# Native Ollama (/api...) has its own downstream normalizer — don't touch it.
|
||||
assert _normalize_chat_endpoint("http://localhost:11434/api") == "http://localhost:11434/api"
|
||||
assert _normalize_chat_endpoint("http://localhost:11434/api/chat") == "http://localhost:11434/api/chat"
|
||||
|
||||
|
||||
def test_empty_and_none_are_passthrough():
|
||||
assert _normalize_chat_endpoint("") == ""
|
||||
assert _normalize_chat_endpoint(None) is None
|
||||
|
||||
|
||||
def test_trailing_slash_base_normalized():
|
||||
assert (
|
||||
_normalize_chat_endpoint("http://localhost:11434/v1/")
|
||||
== "http://localhost:11434/v1/chat/completions"
|
||||
)
|
||||
Reference in New Issue
Block a user