Skip to content
Closed
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
14 changes: 10 additions & 4 deletions langfuse/api/observations/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ def get_many(
## Cursor-based Pagination
This endpoint uses cursor-based pagination for efficient traversal of large datasets.
The cursor is returned in the response metadata and should be passed in subsequent requests
to retrieve the next page of results.
to retrieve the next page of results. The `limit` parameter is a maximum: a page may contain
fewer items than the requested limit, including zero items, while `meta.cursor` is present.
Continue requesting pages until `meta.cursor` is absent.
Requests without a lower start-time bound may search only from January 1, 2020.

## Field Selection
Use the `fields` parameter to control which observation fields are returned:
Expand Down Expand Up @@ -92,7 +95,7 @@ def get_many(
Example: "key1,key2"

limit : typing.Optional[int]
Number of items to return per page. Maximum 1000, default 50.
Maximum number of items to return per page. A page may contain fewer items, including zero items, while meta.cursor is present. Maximum 1000, default 50.

cursor : typing.Optional[str]
Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page.
Expand Down Expand Up @@ -342,7 +345,10 @@ async def get_many(
## Cursor-based Pagination
This endpoint uses cursor-based pagination for efficient traversal of large datasets.
The cursor is returned in the response metadata and should be passed in subsequent requests
to retrieve the next page of results.
to retrieve the next page of results. The `limit` parameter is a maximum: a page may contain
fewer items than the requested limit, including zero items, while `meta.cursor` is present.
Continue requesting pages until `meta.cursor` is absent.
Requests without a lower start-time bound may search only from January 1, 2020.

## Field Selection
Use the `fields` parameter to control which observation fields are returned:
Expand Down Expand Up @@ -378,7 +384,7 @@ async def get_many(
Example: "key1,key2"

limit : typing.Optional[int]
Number of items to return per page. Maximum 1000, default 50.
Maximum number of items to return per page. A page may contain fewer items, including zero items, while meta.cursor is present. Maximum 1000, default 50.

cursor : typing.Optional[str]
Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page.
Expand Down
14 changes: 10 additions & 4 deletions langfuse/api/observations/raw_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ def get_many(
## Cursor-based Pagination
This endpoint uses cursor-based pagination for efficient traversal of large datasets.
The cursor is returned in the response metadata and should be passed in subsequent requests
to retrieve the next page of results.
to retrieve the next page of results. The `limit` parameter is a maximum: a page may contain
fewer items than the requested limit, including zero items, while `meta.cursor` is present.
Continue requesting pages until `meta.cursor` is absent.
Requests without a lower start-time bound may search only from January 1, 2020.

## Field Selection
Use the `fields` parameter to control which observation fields are returned:
Expand Down Expand Up @@ -90,7 +93,7 @@ def get_many(
Example: "key1,key2"

limit : typing.Optional[int]
Number of items to return per page. Maximum 1000, default 50.
Maximum number of items to return per page. A page may contain fewer items, including zero items, while meta.cursor is present. Maximum 1000, default 50.

cursor : typing.Optional[str]
Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page.
Expand Down Expand Up @@ -399,7 +402,10 @@ async def get_many(
## Cursor-based Pagination
This endpoint uses cursor-based pagination for efficient traversal of large datasets.
The cursor is returned in the response metadata and should be passed in subsequent requests
to retrieve the next page of results.
to retrieve the next page of results. The `limit` parameter is a maximum: a page may contain
fewer items than the requested limit, including zero items, while `meta.cursor` is present.
Continue requesting pages until `meta.cursor` is absent.
Requests without a lower start-time bound may search only from January 1, 2020.

## Field Selection
Use the `fields` parameter to control which observation fields are returned:
Expand Down Expand Up @@ -435,7 +441,7 @@ async def get_many(
Example: "key1,key2"

limit : typing.Optional[int]
Number of items to return per page. Maximum 1000, default 50.
Maximum number of items to return per page. A page may contain fewer items, including zero items, while meta.cursor is present. Maximum 1000, default 50.

cursor : typing.Optional[str]
Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page.
Expand Down
5 changes: 5 additions & 0 deletions langfuse/api/observations/types/observations_v2meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class ObservationsV2Meta(CursorMetaResponse):
Metadata for cursor-based pagination.
"""

comment: typing.Optional[str] = pydantic.Field(default=None)
"""
Present when an empty page has a cursor; continue requesting pages until the cursor is absent.
"""
Comment on lines +14 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 pre-existing, not blocking: Batch evaluations over observations can stop partway and still report completed=True with no resume token. The cause is the server's new empty pages that carry a cursor. This spec says a page can be empty while meta.cursor is set, and paging must continue until the cursor is absent. The SDK's own loop at langfuse/batch_evaluation.py:1057 stops on cursor is None or not items, so it treats an empty page as the end. Fix: end the loop only when next_cursor is None, while keeping the max_items stop and the per-page dedupe working. [also at: langfuse/api/observations/client.py:1 - pre-existing: Batch evaluations over observations can stop early and report completion, skipping the remaining observations.]
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

The trigger is a server that sends the contract added at observations_v2meta.py:14-17 and client.py docstrings: data is empty, meta.cursor is set, and comment is present. The spec calls this normal for requests whose bounded scan finds no matches in a slice. The SDK caller run_batched_evaluation on observations fetches pages through _fetch_batch_with_retry (batch_evaluation.py:1115-1123) and at batch_evaluation.py:1057 runs if cursor is None or not items: has_more = False; break. After the break it returns completed=True and has_more_items=False (batch_evaluation.py:1081-1085), and the resume token holds a live cursor that nothing asks the user to follow. Observations after the empty slice are never evaluated and no error is shown. This PR updates the generated client to document the contract but leaves the only in-repo consumer unchanged, so nothing guards against the early stop.

Verification: pre-existing. The trigger is a Langfuse server that returns a page with empty data while meta.cursor is still set. In langfuse/batch_evaluation.py the loop runs if cursor is None or not items: has_more = False; break (lines 1056-1059), so an empty page with a non-null cursor ends the run. The caller gets a "complete" result with no resume token. The base has the same loop.


model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
extra="allow", frozen=True
)
4 changes: 3 additions & 1 deletion langfuse/api/observations/types/observations_v2response.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class ObservationsV2Response(UniversalBaseModel):
Response containing observations with field-group-based filtering and cursor-based pagination.

The `data` array contains observation objects with only the requested field groups included.
Use the `cursor` in `meta` to retrieve the next page of results.
Use the `cursor` in `meta` to retrieve the next page of results. The array may contain fewer
than the requested `limit` items, or be empty, while a cursor is present. Continue until the
cursor is absent.
"""

data: typing.List[ObservationV2] = pydantic.Field()
Expand Down
33 changes: 33 additions & 0 deletions langfuse/api/unstable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,24 @@
)
from .topics import (
Topic,
TopicAssignment,
TopicAssignmentList,
TopicAssignmentStatus,
TopicAssignmentTopic,
TopicFacet,
TopicFacetCounts,
TopicFacetList,
TopicFacetPublishedRun,
TopicFacetRef,
TopicList,
TopicResultListMeta,
TopicResultsOrderBy,
TopicRule,
TopicRuleList,
TopicStatusCounts,
TopicSummary,
TopicSummaryList,
TopicSummaryState,
TopicWindow,
TopicWithStatus,
TopicsOrderBy,
Expand Down Expand Up @@ -115,13 +126,24 @@
"PublicApiValidationIssue": ".errors",
"TooManyRequestsError": ".errors",
"Topic": ".topics",
"TopicAssignment": ".topics",
"TopicAssignmentList": ".topics",
"TopicAssignmentStatus": ".topics",
"TopicAssignmentTopic": ".topics",
"TopicFacet": ".topics",
"TopicFacetCounts": ".topics",
"TopicFacetList": ".topics",
"TopicFacetPublishedRun": ".topics",
"TopicFacetRef": ".topics",
"TopicList": ".topics",
"TopicResultListMeta": ".topics",
"TopicResultsOrderBy": ".topics",
"TopicRule": ".topics",
"TopicRuleList": ".topics",
"TopicStatusCounts": ".topics",
"TopicSummary": ".topics",
"TopicSummaryList": ".topics",
"TopicSummaryState": ".topics",
"TopicWindow": ".topics",
"TopicWithStatus": ".topics",
"TopicsOrderBy": ".topics",
Expand Down Expand Up @@ -207,13 +229,24 @@ def __dir__():
"PublicApiValidationIssue",
"TooManyRequestsError",
"Topic",
"TopicAssignment",
"TopicAssignmentList",
"TopicAssignmentStatus",
"TopicAssignmentTopic",
"TopicFacet",
"TopicFacetCounts",
"TopicFacetList",
"TopicFacetPublishedRun",
"TopicFacetRef",
"TopicList",
"TopicResultListMeta",
"TopicResultsOrderBy",
"TopicRule",
"TopicRuleList",
"TopicStatusCounts",
"TopicSummary",
"TopicSummaryList",
"TopicSummaryState",
"TopicWindow",
"TopicWithStatus",
"TopicsOrderBy",
Expand Down
33 changes: 33 additions & 0 deletions langfuse/api/unstable/topics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,48 @@
if typing.TYPE_CHECKING:
from .types import (
Topic,
TopicAssignment,
TopicAssignmentList,
TopicAssignmentStatus,
TopicAssignmentTopic,
TopicFacet,
TopicFacetCounts,
TopicFacetList,
TopicFacetPublishedRun,
TopicFacetRef,
TopicList,
TopicResultListMeta,
TopicResultsOrderBy,
TopicRule,
TopicRuleList,
TopicStatusCounts,
TopicSummary,
TopicSummaryList,
TopicSummaryState,
TopicWindow,
TopicWithStatus,
TopicsOrderBy,
)
_dynamic_imports: typing.Dict[str, str] = {
"Topic": ".types",
"TopicAssignment": ".types",
"TopicAssignmentList": ".types",
"TopicAssignmentStatus": ".types",
"TopicAssignmentTopic": ".types",
"TopicFacet": ".types",
"TopicFacetCounts": ".types",
"TopicFacetList": ".types",
"TopicFacetPublishedRun": ".types",
"TopicFacetRef": ".types",
"TopicList": ".types",
"TopicResultListMeta": ".types",
"TopicResultsOrderBy": ".types",
"TopicRule": ".types",
"TopicRuleList": ".types",
"TopicStatusCounts": ".types",
"TopicSummary": ".types",
"TopicSummaryList": ".types",
"TopicSummaryState": ".types",
"TopicWindow": ".types",
"TopicWithStatus": ".types",
"TopicsOrderBy": ".types",
Expand Down Expand Up @@ -63,13 +85,24 @@ def __dir__():

__all__ = [
"Topic",
"TopicAssignment",
"TopicAssignmentList",
"TopicAssignmentStatus",
"TopicAssignmentTopic",
"TopicFacet",
"TopicFacetCounts",
"TopicFacetList",
"TopicFacetPublishedRun",
"TopicFacetRef",
"TopicList",
"TopicResultListMeta",
"TopicResultsOrderBy",
"TopicRule",
"TopicRuleList",
"TopicStatusCounts",
"TopicSummary",
"TopicSummaryList",
"TopicSummaryState",
"TopicWindow",
"TopicWithStatus",
"TopicsOrderBy",
Expand Down
Loading
Loading