Skip to content

Commit 53c2e8b

Browse files
Harden Langfuse prompt backend error mapping
From PR #93 review: - Map httpx.TransportError (connect/read/timeout/network) to PromptStoreUnavailable in LangfusePromptBackend, alongside 503s, so PromptManager falls back on transport failures per the PromptBackend contract. Adds a transport-timeout unit test. - Align the opt-in Langfuse integration test's host resolution to the SDK's precedence (LANGFUSE_BASE_URL before LANGFUSE_HOST); it had framed LANGFUSE_HOST as canonical with the opposite precedence.
1 parent 601a13d commit 53c2e8b

3 files changed

Lines changed: 24 additions & 7 deletions

File tree

src/openarmature/prompts/backends/langfuse.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from datetime import UTC, datetime
1818
from typing import Any, Protocol
1919

20+
import httpx
2021
from langfuse.api import NotFoundError, ServiceUnavailableError
2122
from langfuse.model import ChatPromptClient, TextPromptClient
2223

@@ -115,7 +116,12 @@ def _get_prompt(self, name: str, label: str) -> TextPromptClient | ChatPromptCli
115116
label=label,
116117
backend="langfuse",
117118
) from exc
118-
except ServiceUnavailableError as exc:
119+
except (ServiceUnavailableError, httpx.TransportError) as exc:
120+
# 503 plus transport-level failures (connect/read/timeout/
121+
# network): the SDK surfaces raw httpx errors when there's no
122+
# HTTP response to map to a typed error. Per the PromptBackend
123+
# contract these are unavailability, so the manager can fall
124+
# back. 4xx auth and other errors still propagate.
119125
raise PromptStoreUnavailable(
120126
f"Langfuse unavailable fetching ({name!r}, {label!r}): {exc}",
121127
name=name,

tests/unit/test_observability_langfuse_adapter.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
LANGFUSE_PUBLIC_KEY=pk-lf-... \\
1313
LANGFUSE_SECRET_KEY=sk-lf-... \\
14-
LANGFUSE_HOST=https://cloud.langfuse.com \\
14+
LANGFUSE_BASE_URL=https://cloud.langfuse.com \\
1515
uv run pytest tests/unit/test_observability_langfuse_adapter.py \\
1616
-m integration -v
1717
@@ -160,11 +160,11 @@ async def test_adapter_against_real_langfuse_cloud() -> None:
160160
if not public_key or not secret_key:
161161
pytest.skip("LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY not set")
162162

163-
# LANGFUSE_HOST is the canonical name (matches the SDK's ``host=``
164-
# kwarg); LANGFUSE_BASE_URL is the common alias some downstream
165-
# configs use. Accept either; LANGFUSE_HOST wins when both set.
163+
# Mirror the SDK's precedence: Langfuse() reads LANGFUSE_BASE_URL
164+
# first, then LANGFUSE_HOST. Resolve the same order here so this
165+
# explicit host matches what a no-arg Langfuse() would pick up.
166166
host = (
167-
os.environ.get("LANGFUSE_HOST") or os.environ.get("LANGFUSE_BASE_URL") or "https://cloud.langfuse.com"
167+
os.environ.get("LANGFUSE_BASE_URL") or os.environ.get("LANGFUSE_HOST") or "https://cloud.langfuse.com"
168168
)
169169
client = Langfuse(
170170
public_key=public_key,
@@ -175,7 +175,7 @@ async def test_adapter_against_real_langfuse_cloud() -> None:
175175
# background export thread is just a logged warning and the test
176176
# passes while traces vanish.
177177
assert client.auth_check(), (
178-
"Langfuse auth_check failed — verify LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_HOST"
178+
"Langfuse auth_check failed — verify LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL"
179179
)
180180

181181
observer = LangfuseObserver(client=LangfuseSDKAdapter(client))

tests/unit/test_prompts_langfuse.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from typing import Any, cast
66

7+
import httpx
78
import pytest
89

910
pytest.importorskip("langfuse")
@@ -128,6 +129,16 @@ async def test_service_unavailable_maps_to_store_unavailable() -> None:
128129
await backend.fetch("greeting", "production")
129130

130131

132+
async def test_transport_error_maps_to_store_unavailable() -> None:
133+
# A connect/read/timeout/network failure surfaces as a raw httpx
134+
# TransportError (no HTTP response to map to a typed SDK error); it
135+
# must become PromptStoreUnavailable so PromptManager can fall back.
136+
backend = LangfusePromptBackend(_FakeClient(exc=httpx.ConnectTimeout("timed out")))
137+
138+
with pytest.raises(PromptStoreUnavailable):
139+
await backend.fetch("greeting", "production")
140+
141+
131142
async def test_sampling_extracted_from_config() -> None:
132143
client = _text_client(config={"temperature": 0.0, "max_tokens": 256, "model": "gpt-4o"})
133144
backend = LangfusePromptBackend(_FakeClient(result=client))

0 commit comments

Comments
 (0)