Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions langfuse/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,16 @@
ScoreV1_Boolean,
ScoreV1_Categorical,
ScoreV1_Numeric,
ScoreV1_Text,
Score_Boolean,
Score_Categorical,
Score_Correction,
Score_Numeric,
Score_Text,
Session,
SessionWithTraces,
TextScore,
TextScoreV1,
Trace,
TraceWithDetails,
TraceWithFullDetails,
Expand Down Expand Up @@ -281,10 +285,12 @@
GetScoresResponseDataCategorical,
GetScoresResponseDataCorrection,
GetScoresResponseDataNumeric,
GetScoresResponseDataText,
GetScoresResponseData_Boolean,
GetScoresResponseData_Categorical,
GetScoresResponseData_Correction,
GetScoresResponseData_Numeric,
GetScoresResponseData_Text,
GetScoresResponseTraceData,
)
from .sessions import PaginatedSessions
Expand Down Expand Up @@ -377,10 +383,12 @@
"GetScoresResponseDataCategorical": ".scores",
"GetScoresResponseDataCorrection": ".scores",
"GetScoresResponseDataNumeric": ".scores",
"GetScoresResponseDataText": ".scores",
"GetScoresResponseData_Boolean": ".scores",
"GetScoresResponseData_Categorical": ".scores",
"GetScoresResponseData_Correction": ".scores",
"GetScoresResponseData_Numeric": ".scores",
"GetScoresResponseData_Text": ".scores",
"GetScoresResponseTraceData": ".scores",
"HealthResponse": ".health",
"IngestionError": ".ingestion",
Expand Down Expand Up @@ -489,10 +497,12 @@
"ScoreV1_Boolean": ".commons",
"ScoreV1_Categorical": ".commons",
"ScoreV1_Numeric": ".commons",
"ScoreV1_Text": ".commons",
"Score_Boolean": ".commons",
"Score_Categorical": ".commons",
"Score_Correction": ".commons",
"Score_Numeric": ".commons",
"Score_Text": ".commons",
"SdkLogBody": ".ingestion",
"SdkLogEvent": ".ingestion",
"ServiceProviderConfig": ".scim",
Expand All @@ -501,6 +511,8 @@
"SessionWithTraces": ".commons",
"Sort": ".trace",
"TextPrompt": ".prompts",
"TextScore": ".commons",
"TextScoreV1": ".commons",
"Trace": ".commons",
"TraceBody": ".ingestion",
"TraceEvent": ".ingestion",
Expand Down Expand Up @@ -664,10 +676,12 @@ def __dir__():
"GetScoresResponseDataCategorical",
"GetScoresResponseDataCorrection",
"GetScoresResponseDataNumeric",
"GetScoresResponseDataText",
"GetScoresResponseData_Boolean",
"GetScoresResponseData_Categorical",
"GetScoresResponseData_Correction",
"GetScoresResponseData_Numeric",
"GetScoresResponseData_Text",
"GetScoresResponseTraceData",
"HealthResponse",
"IngestionError",
Expand Down Expand Up @@ -776,10 +790,12 @@ def __dir__():
"ScoreV1_Boolean",
"ScoreV1_Categorical",
"ScoreV1_Numeric",
"ScoreV1_Text",
"Score_Boolean",
"Score_Categorical",
"Score_Correction",
"Score_Numeric",
"Score_Text",
"SdkLogBody",
"SdkLogEvent",
"ServiceProviderConfig",
Expand All @@ -788,6 +804,8 @@ def __dir__():
"SessionWithTraces",
"Sort",
"TextPrompt",
"TextScore",
"TextScoreV1",
"Trace",
"TraceBody",
"TraceEvent",
Expand Down
12 changes: 12 additions & 0 deletions langfuse/api/commons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@
ScoreV1_Boolean,
ScoreV1_Categorical,
ScoreV1_Numeric,
ScoreV1_Text,
Score_Boolean,
Score_Categorical,
Score_Correction,
Score_Numeric,
Score_Text,
Session,
SessionWithTraces,
TextScore,
TextScoreV1,
Trace,
TraceWithDetails,
TraceWithFullDetails,
Expand Down Expand Up @@ -110,12 +114,16 @@
"ScoreV1_Boolean": ".types",
"ScoreV1_Categorical": ".types",
"ScoreV1_Numeric": ".types",
"ScoreV1_Text": ".types",
"Score_Boolean": ".types",
"Score_Categorical": ".types",
"Score_Correction": ".types",
"Score_Numeric": ".types",
"Score_Text": ".types",
"Session": ".types",
"SessionWithTraces": ".types",
"TextScore": ".types",
"TextScoreV1": ".types",
"Trace": ".types",
"TraceWithDetails": ".types",
"TraceWithFullDetails": ".types",
Expand Down Expand Up @@ -196,12 +204,16 @@ def __dir__():
"ScoreV1_Boolean",
"ScoreV1_Categorical",
"ScoreV1_Numeric",
"ScoreV1_Text",
"Score_Boolean",
"Score_Categorical",
"Score_Correction",
"Score_Numeric",
"Score_Text",
"Session",
"SessionWithTraces",
"TextScore",
"TextScoreV1",
"Trace",
"TraceWithDetails",
"TraceWithFullDetails",
Expand Down
19 changes: 18 additions & 1 deletion langfuse/api/commons/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,23 @@
Score_Categorical,
Score_Correction,
Score_Numeric,
Score_Text,
)
from .score_config import ScoreConfig
from .score_config_data_type import ScoreConfigDataType
from .score_data_type import ScoreDataType
from .score_source import ScoreSource
from .score_v1 import ScoreV1, ScoreV1_Boolean, ScoreV1_Categorical, ScoreV1_Numeric
from .score_v1 import (
ScoreV1,
ScoreV1_Boolean,
ScoreV1_Categorical,
ScoreV1_Numeric,
ScoreV1_Text,
)
from .session import Session
from .session_with_traces import SessionWithTraces
from .text_score import TextScore
from .text_score_v1 import TextScoreV1
from .trace import Trace
from .trace_with_details import TraceWithDetails
from .trace_with_full_details import TraceWithFullDetails
Expand Down Expand Up @@ -96,12 +105,16 @@
"ScoreV1_Boolean": ".score_v1",
"ScoreV1_Categorical": ".score_v1",
"ScoreV1_Numeric": ".score_v1",
"ScoreV1_Text": ".score_v1",
"Score_Boolean": ".score",
"Score_Categorical": ".score",
"Score_Correction": ".score",
"Score_Numeric": ".score",
"Score_Text": ".score",
"Session": ".session",
"SessionWithTraces": ".session_with_traces",
"TextScore": ".text_score",
"TextScoreV1": ".text_score_v1",
"Trace": ".trace",
"TraceWithDetails": ".trace_with_details",
"TraceWithFullDetails": ".trace_with_full_details",
Expand Down Expand Up @@ -177,12 +190,16 @@ def __dir__():
"ScoreV1_Boolean",
"ScoreV1_Categorical",
"ScoreV1_Numeric",
"ScoreV1_Text",
"Score_Boolean",
"Score_Categorical",
"Score_Correction",
"Score_Numeric",
"Score_Text",
"Session",
"SessionWithTraces",
"TextScore",
"TextScoreV1",
"Trace",
"TraceWithDetails",
"TraceWithFullDetails",
Expand Down
49 changes: 48 additions & 1 deletion langfuse/api/commons/types/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,54 @@ class Score_Correction(UniversalBaseModel):
)


class Score_Text(UniversalBaseModel):
data_type: typing_extensions.Annotated[
typing.Literal["TEXT"], FieldMetadata(alias="dataType")
] = "TEXT"
string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")]
id: str
trace_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="traceId")
] = None
session_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="sessionId")
] = None
observation_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="observationId")
] = None
dataset_run_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="datasetRunId")
] = None
name: str
source: ScoreSource
timestamp: dt.datetime
created_at: typing_extensions.Annotated[
dt.datetime, FieldMetadata(alias="createdAt")
]
updated_at: typing_extensions.Annotated[
dt.datetime, FieldMetadata(alias="updatedAt")
]
author_user_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="authorUserId")
] = None
comment: typing.Optional[str] = None
metadata: typing.Any
config_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="configId")
] = None
queue_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="queueId")
] = None
environment: str

model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
extra="allow", frozen=True
)


Score = typing_extensions.Annotated[
typing.Union[Score_Numeric, Score_Categorical, Score_Boolean, Score_Correction],
typing.Union[
Score_Numeric, Score_Categorical, Score_Boolean, Score_Correction, Score_Text
],
pydantic.Field(discriminator="data_type"),
]
4 changes: 4 additions & 0 deletions langfuse/api/commons/types/score_config_data_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ class ScoreConfigDataType(enum.StrEnum):
NUMERIC = "NUMERIC"
BOOLEAN = "BOOLEAN"
CATEGORICAL = "CATEGORICAL"
TEXT = "TEXT"

def visit(
self,
numeric: typing.Callable[[], T_Result],
boolean: typing.Callable[[], T_Result],
categorical: typing.Callable[[], T_Result],
text: typing.Callable[[], T_Result],
) -> T_Result:
if self is ScoreConfigDataType.NUMERIC:
return numeric()
if self is ScoreConfigDataType.BOOLEAN:
return boolean()
if self is ScoreConfigDataType.CATEGORICAL:
return categorical()
if self is ScoreConfigDataType.TEXT:
return text()
4 changes: 4 additions & 0 deletions langfuse/api/commons/types/score_data_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ class ScoreDataType(enum.StrEnum):
BOOLEAN = "BOOLEAN"
CATEGORICAL = "CATEGORICAL"
CORRECTION = "CORRECTION"
TEXT = "TEXT"
Comment thread
wochinge marked this conversation as resolved.

def visit(
self,
numeric: typing.Callable[[], T_Result],
boolean: typing.Callable[[], T_Result],
categorical: typing.Callable[[], T_Result],
correction: typing.Callable[[], T_Result],
text: typing.Callable[[], T_Result],
) -> T_Result:
if self is ScoreDataType.NUMERIC:
return numeric()
Comment on lines 18 to 26
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 The visit() methods on ScoreDataType and ScoreConfigDataType add a new mandatory text parameter without a default value, which will raise TypeError at the call site for any existing user code that calls these methods without the new text argument. Any SDK consumer who uses visit()-based score type dispatch (e.g. score_data_type.visit(numeric=..., boolean=..., categorical=..., correction=...)) will have silently broken code after upgrading to this version — this breaking change should be explicitly called out in a changelog entry or semver bump.

Extended reasoning...

What the bug is and how it manifests

The ScoreDataType.visit() method in langfuse/api/commons/types/score_data_type.py previously accepted four callables: numeric, boolean, categorical, and correction. This PR adds a fifth mandatory parameter text with no default value. Similarly, ScoreConfigDataType.visit() in score_config_data_type.py previously took three callables and now requires a fourth mandatory text parameter. Python raises TypeError at the call site when any required keyword argument is missing, regardless of which enum value self currently holds.

The specific code path that triggers it

Any existing caller that pattern-matches on score types via visit() will fail at runtime:

# Previously valid; now raises TypeError on upgrade:
score_data_type.visit(
    numeric=lambda: "numeric",
    boolean=lambda: "bool",
    categorical=lambda: "cat",
    correction=lambda: "corr",
)
# TypeError: visit() missing 1 required keyword argument: text

Python validates all required arguments before dispatching the call, so even if the ScoreDataType value is NUMERIC and text would never be invoked, the TypeError is raised immediately at the call site.

Addressing the refutation

The refuter is correct that the Fern exhaustive visitor pattern is intentionally designed this way: adding a new enum value is a breaking change for visit() callers, and requiring the new handler prevents silent misbehavior (where TEXT scores would be silently ignored). Adding text=None as a default would indeed be a regression — code that does not handle TEXT scores would silently produce wrong results. So the code itself is correct per Fern design contract.

Why this still deserves attention

While the code behavior is intentional, the practical impact is real: this is a breaking change on the public langfuse.api surface. The methods are exported via langfuse/api/init.py and langfuse/api/commons/init.py. Any user who has written visit()-based dispatch code will encounter a runtime TypeError after upgrading. The PR description says no manually editable code was modified, which is accurate but does not negate the user-visible break.

Impact

SDK users who use the Fern client low-level visit() pattern for exhaustive score-type dispatch will have broken code after upgrading. The high-level Langfuse() client does not call visit() internally, so server-side functionality works correctly. The impact is limited to users of the langfuse.api low-level layer who pattern-match on ScoreDataType or ScoreConfigDataType values.

Step-by-step proof

  1. User has existing code: score.data_type.visit(numeric=..., boolean=..., categorical=..., correction=...)
  2. User upgrades the SDK to this version.
  3. ScoreDataType.visit now requires text as a fifth mandatory keyword argument.
  4. Python raises TypeError: visit() missing 1 required keyword argument: text at the call site before any dispatch logic runs.
  5. The user must add text=lambda: ... to every visit() call site to restore functionality.

How to address

The code change itself is correct per Fern exhaustive visitor design. The appropriate remedy is not to change the generated code, but to ensure this breaking change is documented explicitly in the CHANGELOG as a breaking change requiring a major or minor semver bump, and that a migration note is added instructing users to add text=lambda: ... handlers to all visit() call sites.

Expand All @@ -28,3 +30,5 @@ def visit(
return categorical()
if self is ScoreDataType.CORRECTION:
return correction()
if self is ScoreDataType.TEXT:
return text()
39 changes: 38 additions & 1 deletion langfuse/api/commons/types/score_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,44 @@ class ScoreV1_Boolean(UniversalBaseModel):
)


class ScoreV1_Text(UniversalBaseModel):
data_type: typing_extensions.Annotated[
typing.Literal["TEXT"], FieldMetadata(alias="dataType")
] = "TEXT"
string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")]
id: str
trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")]
name: str
source: ScoreSource
observation_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="observationId")
] = None
timestamp: dt.datetime
created_at: typing_extensions.Annotated[
dt.datetime, FieldMetadata(alias="createdAt")
]
updated_at: typing_extensions.Annotated[
dt.datetime, FieldMetadata(alias="updatedAt")
]
author_user_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="authorUserId")
] = None
comment: typing.Optional[str] = None
metadata: typing.Any
config_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="configId")
] = None
queue_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="queueId")
] = None
environment: str

model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
extra="allow", frozen=True
)


ScoreV1 = typing_extensions.Annotated[
typing.Union[ScoreV1_Numeric, ScoreV1_Categorical, ScoreV1_Boolean],
typing.Union[ScoreV1_Numeric, ScoreV1_Categorical, ScoreV1_Boolean, ScoreV1_Text],
pydantic.Field(discriminator="data_type"),
]
21 changes: 21 additions & 0 deletions langfuse/api/commons/types/text_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This file was auto-generated by Fern from our API Definition.

import typing

import pydantic
import typing_extensions
from ...core.serialization import FieldMetadata
from .base_score import BaseScore


class TextScore(BaseScore):
string_value: typing_extensions.Annotated[
str, FieldMetadata(alias="stringValue")
] = pydantic.Field()
"""
The text content of the score (1-500 characters)
"""

model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
extra="allow", frozen=True
)
21 changes: 21 additions & 0 deletions langfuse/api/commons/types/text_score_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This file was auto-generated by Fern from our API Definition.

import typing

import pydantic
import typing_extensions
from ...core.serialization import FieldMetadata
from .base_score_v1 import BaseScoreV1


class TextScoreV1(BaseScoreV1):
string_value: typing_extensions.Annotated[
str, FieldMetadata(alias="stringValue")
] = pydantic.Field()
"""
The text content of the score (1-500 characters)
"""

model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
extra="allow", frozen=True
)
Loading
Loading