Skip to content
Open
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
103 changes: 97 additions & 6 deletions langfuse/_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
ScoreBody,
TraceBody,
)
from langfuse.api.core.api_error import ApiError
from langfuse.batch_evaluation import (
BatchEvaluationResult,
BatchEvaluationResumeToken,
Expand Down Expand Up @@ -176,6 +177,73 @@ def _serialize_evaluations(evaluations: List[Evaluation]) -> List[Dict[str, Any]
]


_V4_REJECTION_MARKER = "not available on deployments running in Langfuse v4"

_V4_DATASET_RUN_HINT = (
"This dataset-run endpoint is not available on Langfuse v4 (events_only) "
"deployments. Read runs with `client.api.experiments.list(...)` and "
"`client.api.experiments.list_items(...)` instead -- both need "
"`from_start_time`, and `dataset_run_id` from "
"`run_experiment()` is the same value those return as `id` / `experimentId`. "
"See https://langfuse.com/docs/v4 for the migration guide."
)

_V4_DELETE_HINT = (
"Deleting a dataset run is not available on Langfuse v4 (events_only) "
"deployments, and there is no delete counterpart on `client.api.experiments` "
"-- the run itself is untouched. See https://langfuse.com/docs/v4 for the "
"migration guide."
)


def _handle_dataset_run_error(exc: ApiError, hint: str = _V4_DATASET_RUN_HINT) -> None:
"""Route a failed dataset-run call: v4 guidance, silence for 404, else log.

A Langfuse v4 deployment answers the legacy dataset-run endpoints with 404,
and the generated exception's ``str()`` starts with the response headers, so
the server's own explanation is only reachable through ``exc.body``. A caller
that just prints the exception therefore sees no hint at all.

404 is deliberately not routed to ``handle_fern_exception``: its 404 entry
reads "Internal error occurred ... we are monitoring it closely", and these
helpers raise ``NotFoundError`` routinely, because asking for a run that does
not exist is an ordinary outcome.

The three helpers share this so the marker is checked once per failure and so
each can pass the hint that is actually true for it.
"""
if _is_v4_dataset_run_rejection(exc):
langfuse_logger.warning(hint)
elif not _is_not_found(exc):
handle_fern_exception(exc)


def _is_v4_dataset_run_rejection(exc: Exception) -> bool:
"""Whether the server refused the call because the deployment is v4."""
body = getattr(exc, "body", None)
text = body if isinstance(body, str) else str(body or "")
return _V4_REJECTION_MARKER in text


def _is_not_found(exc: Exception) -> bool:
"""Whether the server answered 404.

``handle_fern_exception`` maps a bare status onto a generic message, and its
404 entry reads "Internal error occurred. This is an unusual occurrence and
we are monitoring it closely". These three helpers raise ``NotFoundError``
routinely -- asking for a run that does not exist is an ordinary outcome, not
an internal error -- so routing 404 through it would ship a false claim at
ERROR level, which is also what error alerting keys on.
"""
status = getattr(exc, "status_code", None)
if status is None:
return False
try:
return int(status) == 404
except (TypeError, ValueError):
return False


class Langfuse:
"""Main client for Langfuse tracing and platform features.

Expand Down Expand Up @@ -2534,6 +2602,16 @@ def get_dataset_run(
) -> DatasetRunWithItems:
"""Fetch a dataset run by dataset name and run name.

Not available on Langfuse v4 deployments: the underlying
``GET /api/public/datasets/{name}/runs/{run_name}`` path is rejected
with 404 in v4 ``events_only`` mode. Use
``client.api.experiments.list(from_start_time=..., dataset_id=...)``
instead -- ``from_start_time`` is required, ``dataset_id`` scopes the
lookup the way ``dataset_name`` did here, and matching on ``name`` alone
is ambiguous when two datasets reuse a run name. Prefer matching on
``id``: ``run_experiment()`` returns the same value as
``dataset_run_id``. See https://langfuse.com/docs/v4.

Args:
dataset_name (str): The name of the dataset.
run_name (str): The name of the run.
Expand All @@ -2550,8 +2628,8 @@ def get_dataset_run(
request_options=None,
),
)
except Error as e:
handle_fern_exception(e)
except ApiError as e:
_handle_dataset_run_error(e)
raise e

def get_dataset_runs(
Expand All @@ -2563,6 +2641,14 @@ def get_dataset_runs(
) -> PaginatedDatasetRuns:
"""Fetch all runs for a dataset.

Not available on Langfuse v4 deployments: the underlying
``GET /api/public/datasets/{name}/runs`` path is rejected with 404 in v4
``events_only`` mode. Use
``client.api.experiments.list(from_start_time=..., dataset_id=...)``
instead; note it is cursor-paginated, requires ``from_start_time``, and
filters by ``datasetId`` rather than dataset name. See
https://langfuse.com/docs/v4.

Args:
dataset_name (str): The name of the dataset.
page (Optional[int]): Page number, starts at 1.
Expand All @@ -2581,15 +2667,20 @@ def get_dataset_runs(
request_options=None,
),
)
except Error as e:
handle_fern_exception(e)
except ApiError as e:
_handle_dataset_run_error(e)
raise e

def delete_dataset_run(
self, *, dataset_name: str, run_name: str
) -> DeleteDatasetRunResponse:
"""Delete a dataset run and all its run items. This action is irreversible.

Not available on Langfuse v4 deployments: the underlying
``DELETE /api/public/datasets/{name}/runs/{run_name}`` path is rejected
with 404 in v4 ``events_only`` mode, and there is no delete counterpart
on ``client.api.experiments``. See https://langfuse.com/docs/v4.

Args:
dataset_name (str): The name of the dataset.
run_name (str): The name of the run.
Expand All @@ -2606,8 +2697,8 @@ def delete_dataset_run(
request_options=None,
),
)
except Error as e:
handle_fern_exception(e)
except ApiError as e:
_handle_dataset_run_error(e, hint=_V4_DELETE_HINT)
raise e

def run_experiment(
Expand Down
11 changes: 8 additions & 3 deletions langfuse/_utils/parse_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
# fern api errors
from langfuse.api import (
AccessDeniedError,
Error,
MethodNotAllowedError,
NotFoundError,
ServiceUnavailableError,
Expand Down Expand Up @@ -44,7 +43,13 @@
}


def generate_error_message_fern(error: Error) -> str:
def generate_error_message_fern(error: ApiError) -> str:
"""Message for a raised API error.

Takes ``ApiError`` rather than the narrower generated ``Error``: every
generated error is an ``ApiError``, but not the other way round, and the
body already dispatches with ``isinstance(..., ApiError)``.
"""
if isinstance(error, AccessDeniedError):
return errorResponseByCode.get(403, defaultErrorResponse)
elif isinstance(error, MethodNotAllowedError):
Expand All @@ -66,7 +71,7 @@ def generate_error_message_fern(error: Error) -> str:
return defaultErrorResponse # type: ignore


def handle_fern_exception(exception: Error) -> None:
def handle_fern_exception(exception: ApiError) -> None:
logger.debug(exception)
error_message = generate_error_message_fern(exception)
logger.error(error_message)
Expand Down
Loading