{"record":{"id":"83adb527da899b02","repo":"odysseus-dev/odysseus","slug":"no-endpoint-configured-for-ai-tidy","errorCode":null,"errorMessage":"No endpoint configured for AI tidy","messagePattern":"No endpoint configured for AI tidy","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/document/document_routes.py","lineNumber":981,"sourceCode":"        finally:\n            db.close()\n\n    # ---- POST /api/documents/ai-tidy — AI-powered cleanup of junk/test documents ----\n    @router.post(\"/api/documents/ai-tidy\")\n    async def ai_tidy_documents(request: Request) -> Dict[str, Any]:\n        \"\"\"Use AI to judge if documents are junk/test/accidental, then delete them.\n        Caches verdicts so previously-reviewed docs are skipped.\"\"\"\n        from src.task_endpoint import resolve_task_endpoint\n        from src.endpoint_resolver import resolve_endpoint\n        from src.llm_core import llm_call_async\n\n        user = get_current_user(request)\n        url, model, headers = resolve_task_endpoint(owner=user or None)\n        if not url or not model:\n            # Fall back to default endpoint\n            url, model, headers = resolve_endpoint(\"default\", owner=user or None)\n        if not url or not model:\n            raise HTTPException(500, \"No endpoint configured for AI tidy\")\n\n        db = SessionLocal()\n        try:\n            q = (\n                db.query(Document)\n                .outerjoin(DbSession, Document.session_id == DbSession.id)\n                .filter(Document.is_active == True)\n                .filter((Document.archived == False) | (Document.archived.is_(None)))\n            )\n            q = _owner_session_filter(q, user)\n            docs = q.all()\n\n            # Only review docs that haven't been reviewed yet\n            to_review = [d for d in docs if not d.tidy_verdict]\n            if not to_review:\n                return {\"deleted\": 0, \"reviewed\": 0, \"message\": \"All documents already reviewed\"}\n\n            # Build a batch prompt — review up to 30 at a time","sourceCodeStart":963,"sourceCodeEnd":999,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/document/document_routes.py#L963-L999","documentation":"Raised by POST /api/documents/ai-tidy when neither resolve_task_endpoint(owner=user) nor the fallback resolve_endpoint('default', owner=user) returns both a url and a model. It is a configuration 500: the AI-classification step cannot run because no LLM endpoint is registered for this owner (or globally as default).","triggerScenarios":"Calling ai-tidy on a fresh install where no endpoints are configured; the user-specific endpoint row exists but has an empty model field; endpoint config was cleared or its owner scope does not cover the current user; running under a service account with no default endpoint.","commonSituations":"New deployment before provider keys/endpoints are entered; per-user endpoint settings partially saved (url set, model blank); environment-specific config not migrated to production.","solutions":["Configure a task/default endpoint via the app's endpoint settings UI or resolver config so resolve_endpoint('default') returns url+model.","Verify the owner scoping: either set a default endpoint that applies to all users or one bound to the calling user.","Confirm both url AND model are non-empty — the check fails if either is blank.","As a fallback, use POST /api/documents/tidy (non-AI) which needs no endpoint."],"exampleFix":"# before\nresp = requests.post(f\"{base}/api/documents/ai-tidy\")  # 500 if unconfigured\n\n# after\nurl, model, _ = resolve_endpoint(\"default\", owner=user or None)\nif not url or not model:\n    resp = requests.post(f\"{base}/api/documents/tidy\")  # heuristic cleanup instead\nelse:\n    resp = requests.post(f\"{base}/api/documents/ai-tidy\")","handlingStrategy":"validation","validationCode":"url, model, _ = resolve_endpoint(\"default\", owner=user or None)\nif not url or not model:\n    use_plain_tidy = True  # no AI endpoint configured","typeGuard":null,"tryCatchPattern":"try:\n    r = requests.post(f\"{base}/api/documents/ai-tidy\")\nexcept requests.HTTPError as e:\n    if e.response.status_code == 500 and \"No endpoint configured\" in e.response.json().get(\"detail\", \"\"):\n        r = requests.post(f\"{base}/api/documents/tidy\")  # non-AI fallback\n    else:\n        raise","preventionTips":["Configure a default LLM endpoint (url AND model) before enabling ai-tidy.","Preflight-check endpoint config in the UI and gray out AI tidy when missing.","Remember per-user endpoints need the owner scope to match the caller."],"tags":["fastapi","configuration","llm","http-500","maintenance"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}