From 3cff06781e47390afd52313e80397e412da2ac3e Mon Sep 17 00:00:00 2001 From: Vykos Date: Sun, 7 Jun 2026 12:40:23 +0200 Subject: [PATCH] Scope model helper endpoint resolution (#3007) --- routes/calendar_routes.py | 6 ++-- routes/document_routes.py | 4 +-- routes/history_routes.py | 4 ++- routes/note_routes.py | 4 +-- routes/task_routes.py | 5 +-- tests/test_model_helper_owner_scope.py | 45 ++++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 tests/test_model_helper_owner_scope.py diff --git a/routes/calendar_routes.py b/routes/calendar_routes.py index 75b6a5715..7aa812c25 100644 --- a/routes/calendar_routes.py +++ b/routes/calendar_routes.py @@ -1368,7 +1368,7 @@ def setup_calendar_routes() -> APIRouter: "tomorrow", "next Tuesday", "in 30 minutes" resolve correctly. Uses the "utility" endpoint (small / fast model) to keep latency low. """ - _require_user(request) + owner = _require_user(request) from src.endpoint_resolver import resolve_endpoint from src.llm_core import llm_call_async from src.text_helpers import strip_think @@ -1394,9 +1394,9 @@ def setup_calendar_routes() -> APIRouter: if tz_hint: set_user_tz_name(tz_hint) - url, model, headers = resolve_endpoint("utility") + url, model, headers = resolve_endpoint("utility", owner=owner or None) if not url: - url, model, headers = resolve_endpoint("default") + url, model, headers = resolve_endpoint("default", owner=owner or None) if not url or not model: return {"ok": False, "error": "No LLM endpoint configured"} diff --git a/routes/document_routes.py b/routes/document_routes.py index b4f6aad77..38566dfc6 100644 --- a/routes/document_routes.py +++ b/routes/document_routes.py @@ -853,10 +853,10 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: from src.llm_core import llm_call_async user = get_current_user(request) - url, model, headers = resolve_task_endpoint() + url, model, headers = resolve_task_endpoint(owner=user or None) if not url or not model: # Fall back to default endpoint - url, model, headers = resolve_endpoint("default") + url, model, headers = resolve_endpoint("default", owner=user or None) if not url or not model: raise HTTPException(500, "No endpoint configured for AI tidy") diff --git a/routes/history_routes.py b/routes/history_routes.py index 35aaff2a8..378fab35f 100644 --- a/routes/history_routes.py +++ b/routes/history_routes.py @@ -522,6 +522,8 @@ def setup_history_routes(session_manager) -> APIRouter: async def compact_session(request: Request, session_id: str): """Manually trigger context compaction for a session.""" _verify_session_owner(request, session_id) + from src.auth_helpers import effective_user + owner = effective_user(request) try: session = session_manager.get_session(session_id) except KeyError: @@ -555,7 +557,7 @@ def setup_history_routes(session_manager) -> APIRouter: ) # Use utility model if available - util_url, util_model, util_headers = resolve_endpoint("utility") + util_url, util_model, util_headers = resolve_endpoint("utility", owner=owner or None) compact_url = util_url or session.endpoint_url compact_model = util_model or session.model compact_headers = util_headers if util_url else session.headers diff --git a/routes/note_routes.py b/routes/note_routes.py index 3ad002fb4..947788a42 100644 --- a/routes/note_routes.py +++ b/routes/note_routes.py @@ -181,9 +181,9 @@ async def dispatch_reminder( try: from src.endpoint_resolver import resolve_endpoint from src.llm_core import llm_call_async - url, model, headers = resolve_endpoint("utility") + url, model, headers = resolve_endpoint("utility", owner=owner or None) if not url: - url, model, headers = resolve_endpoint("default") + url, model, headers = resolve_endpoint("default", owner=owner or None) if url and model: raw = await llm_call_async( url=url, model=model, diff --git a/routes/task_routes.py b/routes/task_routes.py index 66049237d..dfaed0808 100644 --- a/routes/task_routes.py +++ b/routes/task_routes.py @@ -1047,6 +1047,7 @@ def setup_task_routes(task_scheduler) -> APIRouter: desc = (body.get("description") or "").strip() if not desc: return {"success": False, "message": "Nothing to parse"} + user = _owner(request) now = _dt.now() # Give the model the current date/time + weekday so relative phrasing @@ -1073,9 +1074,9 @@ def setup_task_routes(task_scheduler) -> APIRouter: "use cron '0 H * * 1-5'. Keep the prompt actionable and self-contained." ) try: - url, model, headers = resolve_endpoint("utility") + url, model, headers = resolve_endpoint("utility", owner=user or None) if not url: - url, model, headers = resolve_endpoint("default") + url, model, headers = resolve_endpoint("default", owner=user or None) if not (url and model): return {"success": False, "message": "No model endpoint configured"} raw = await llm_call_async( diff --git a/tests/test_model_helper_owner_scope.py b/tests/test_model_helper_owner_scope.py new file mode 100644 index 000000000..4612fa363 --- /dev/null +++ b/tests/test_model_helper_owner_scope.py @@ -0,0 +1,45 @@ +"""Model-assisted route helpers must resolve endpoints with owner scope.""" + +import ast +from pathlib import Path + + +def _function_source(path: str, name: str) -> str: + source = Path(path).read_text(encoding="utf-8") + tree = ast.parse(source) + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name: + return ast.get_source_segment(source, node) or "" + raise AssertionError(f"{name} not found in {path}") + + +def test_document_ai_tidy_resolves_with_owner_scope(): + body = _function_source("routes/document_routes.py", "ai_tidy_documents") + assert "resolve_task_endpoint(owner=user or None)" in body + assert 'resolve_endpoint("default", owner=user or None)' in body + + +def test_calendar_quick_parse_resolves_with_owner_scope(): + body = _function_source("routes/calendar_routes.py", "quick_parse") + assert "owner = _require_user(request)" in body + assert 'resolve_endpoint("utility", owner=owner or None)' in body + assert 'resolve_endpoint("default", owner=owner or None)' in body + + +def test_task_parse_resolves_with_owner_scope(): + body = _function_source("routes/task_routes.py", "parse_task") + assert "user = _owner(request)" in body + assert 'resolve_endpoint("utility", owner=user or None)' in body + assert 'resolve_endpoint("default", owner=user or None)' in body + + +def test_history_compact_resolves_with_owner_scope(): + body = _function_source("routes/history_routes.py", "compact_session") + assert "owner = effective_user(request)" in body + assert 'resolve_endpoint("utility", owner=owner or None)' in body + + +def test_note_reminder_synthesis_resolves_with_owner_scope(): + body = _function_source("routes/note_routes.py", "dispatch_reminder") + assert 'resolve_endpoint("utility", owner=owner or None)' in body + assert 'resolve_endpoint("default", owner=owner or None)' in body