Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ All notable changes to Palinode. Format follows [Keep a Changelog](https://keepa

### Fixed

- Preserve the embedding-backend 503 response for `/triggers` and
`/check-triggers` instead of converting outages into generic 500 errors.

### Removed

### Security
Expand Down
13 changes: 13 additions & 0 deletions palinode/api/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from fastapi import HTTPException

from palinode.core import embedder
from palinode.core.config import config
from palinode.core.retrieval_log import RetrievalLogger

Expand All @@ -43,6 +44,18 @@ def _safe_500(e: Exception, context: str = "Internal error") -> HTTPException:
return HTTPException(status_code=500, detail=context)


def _embedding_unavailable_503(
error: embedder.EmbeddingUnavailable,
operation: str,
) -> HTTPException:
"""Preserve the typed outage message without logging a traceback."""
logger.warning(
"%s unavailable outcome=embedding_unavailable",
operation,
)
return HTTPException(status_code=503, detail=str(error))


def _project_from_cwd(cwd: str | None) -> str | None:
"""Derive a project slug from a CWD path's basename.

Expand Down
18 changes: 5 additions & 13 deletions palinode/api/routers/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
from palinode.core.config import config
from palinode.core.parity import CATEGORIES, MEMORY_TYPES
from palinode.core.path_guard import to_rel_path
from palinode.api._util import _retrieval_logger, _safe_500
from palinode.api._util import (
_embedding_unavailable_503,
_retrieval_logger,
_safe_500,
)
from palinode.api.rate_limit import _RATE_LIMIT_SEARCH, _check_rate_limit
from palinode.api.search_helpers import (
_compute_effective_date_after,
Expand All @@ -32,18 +36,6 @@
_VISIBILITY_OVERFETCH = 5


def _embedding_unavailable_503(
error: embedder.EmbeddingUnavailable,
operation: str,
) -> HTTPException:
"""Preserve the typed outage message without logging a traceback."""
logger.warning(
"%s unavailable op=search outcome=embedding_unavailable",
operation,
)
return HTTPException(status_code=503, detail=str(error))


def _resolve_scope_chain(
context: list[str] | None = None,
session_id: str | None = None,
Expand Down
6 changes: 5 additions & 1 deletion palinode/api/routers/triggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from fastapi import APIRouter
from pydantic import BaseModel

from palinode.api._util import _safe_500
from palinode.api._util import _embedding_unavailable_503, _safe_500
from palinode.core import embedder, store

router = APIRouter()
Expand Down Expand Up @@ -43,6 +43,8 @@ def create_trigger_api(req: TriggerRequest) -> dict[str, Any]:
cooldown_hours=req.cooldown_hours or 24
)
return {"id": trigger_id, "status": "created"}
except embedder.EmbeddingUnavailable as e:
raise _embedding_unavailable_503(e, "Trigger creation") from None
except Exception as e:
raise _safe_500(e, "Trigger creation failed")

Expand Down Expand Up @@ -72,5 +74,7 @@ def check_triggers_api(req: CheckTriggersRequest) -> list[dict[str, Any]]:
cooldown_bypass=req.cooldown_bypass or False
)
return results
except embedder.EmbeddingUnavailable as e:
raise _embedding_unavailable_503(e, "Trigger check") from None
except Exception as e:
raise _safe_500(e, "Trigger check failed")
27 changes: 27 additions & 0 deletions tests/test_search_embedder_outage.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,33 @@ def test_other_embedding_search_operations_preserve_typed_503(
assert "palinode doctor" in res.json()["detail"]


def test_create_trigger_preserves_typed_embedding_503(client):
with patch("palinode.core.embedder.embed", side_effect=_boom):
res = client.post(
"/triggers",
json={
"description": "deploy the memory service",
"memory_file": "projects/memory.md",
},
)

assert res.status_code == 503
assert "Embedding backend unavailable" in res.json()["detail"]
assert "palinode doctor" in res.json()["detail"]


def test_check_triggers_preserves_typed_embedding_503(client):
with patch("palinode.core.embedder.embed", side_effect=_boom):
res = client.post(
"/check-triggers",
json={"query": "what should I remember about deployment?"},
)

assert res.status_code == 503
assert "Embedding backend unavailable" in res.json()["detail"]
assert "palinode doctor" in res.json()["detail"]


def test_save_keeps_200_and_index_error_with_same_dead_embedder(client, dead_embedder):
res = client.post(
"/save",
Expand Down
Loading