From f2d9e4b891619b02456d9702dd0d9a0f3ef23a8e Mon Sep 17 00:00:00 2001 From: Niklas Semmler Date: Tue, 10 Nov 2026 18:19:13 +0100 Subject: [PATCH 1/2] fix(api): make null filter value optional and align observations filter docs Regenerated client changes for langfuse/langfuse ccfda83 (#20029) and a65fe03 (#20026) only, split out of the bot sync PR #2122. Co-Authored-By: Claude Opus 5.5 --- .../types/evaluation_rule_filter.py | 2 +- .../types/null_evaluation_rule_filter.py | 4 +-- langfuse/api/observations/client.py | 28 ++++++++----------- langfuse/api/observations/raw_client.py | 28 ++++++++----------- .../types/dashboard_widget_filter.py | 2 +- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py b/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py index 160026a20..5e38561c7 100644 --- a/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py +++ b/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py @@ -647,7 +647,7 @@ class EvaluationRuleFilter_Null(UniversalBaseModel): type: typing.Literal["null"] = "null" column: str operator: EvaluationRuleNullFilterOperator - value: typing.Literal[""] = "" + value: typing.Optional[typing.Literal[""]] = None model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py b/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py index 17343d87b..8fafb3ca5 100644 --- a/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py +++ b/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py @@ -14,9 +14,9 @@ class NullEvaluationRuleFilter(UniversalBaseModel): """ operator: EvaluationRuleNullFilterOperator - value: typing.Literal[""] = pydantic.Field(default="") + value: typing.Optional[typing.Literal[""]] = pydantic.Field(default=None) """ - Required empty-string placeholder used by the runtime filter contract. + Optional. Defaults to an empty string. """ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( diff --git a/langfuse/api/observations/client.py b/langfuse/api/observations/client.py index 5e717adb6..8ffae967a 100644 --- a/langfuse/api/observations/client.py +++ b/langfuse/api/observations/client.py @@ -146,21 +146,19 @@ def get_many( ```json [ { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "arrayOptions", "stringObject", "boolean", "null" "column": string, // Required. Column to filter on (see available columns below) "operator": string, // Required. Operator based on type: // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "is not empty", "matches" (input/output only) + // - stringOptions: "any of", "none of" (works on any string column except input/output) // - arrayOptions: "any of", "none of", "all of" // - number: "=", ">", "<", ">=", "<=" // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" // - boolean: "=", "<>" // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + "value": any, // Required. Type depends on filter type: ISO 8601 string for datetime, JSON number for number (not a numeric string), array of strings for stringOptions/arrayOptions, boolean for boolean, and "" (empty string) for null + "key": string // Required only for stringObject (the metadata key to filter on) } ] ``` @@ -210,7 +208,7 @@ def get_many( ### Structured Data - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + - `metadata` (stringObject) - Metadata key-value pairs. Use the `key` parameter to filter on a specific metadata key. Metadata values are stored as strings, so match numbers and booleans by their string form with `=` (for example `"value": "true"` or `"value": "42"`); numeric comparisons such as `>` are not supported on metadata. The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. @@ -432,21 +430,19 @@ async def get_many( ```json [ { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "arrayOptions", "stringObject", "boolean", "null" "column": string, // Required. Column to filter on (see available columns below) "operator": string, // Required. Operator based on type: // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "is not empty", "matches" (input/output only) + // - stringOptions: "any of", "none of" (works on any string column except input/output) // - arrayOptions: "any of", "none of", "all of" // - number: "=", ">", "<", ">=", "<=" // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" // - boolean: "=", "<>" // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + "value": any, // Required. Type depends on filter type: ISO 8601 string for datetime, JSON number for number (not a numeric string), array of strings for stringOptions/arrayOptions, boolean for boolean, and "" (empty string) for null + "key": string // Required only for stringObject (the metadata key to filter on) } ] ``` @@ -496,7 +492,7 @@ async def get_many( ### Structured Data - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + - `metadata` (stringObject) - Metadata key-value pairs. Use the `key` parameter to filter on a specific metadata key. Metadata values are stored as strings, so match numbers and booleans by their string form with `=` (for example `"value": "true"` or `"value": "42"`); numeric comparisons such as `>` are not supported on metadata. The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. diff --git a/langfuse/api/observations/raw_client.py b/langfuse/api/observations/raw_client.py index 84dba4891..08a171c7c 100644 --- a/langfuse/api/observations/raw_client.py +++ b/langfuse/api/observations/raw_client.py @@ -144,21 +144,19 @@ def get_many( ```json [ { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "arrayOptions", "stringObject", "boolean", "null" "column": string, // Required. Column to filter on (see available columns below) "operator": string, // Required. Operator based on type: // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "is not empty", "matches" (input/output only) + // - stringOptions: "any of", "none of" (works on any string column except input/output) // - arrayOptions: "any of", "none of", "all of" // - number: "=", ">", "<", ">=", "<=" // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" // - boolean: "=", "<>" // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + "value": any, // Required. Type depends on filter type: ISO 8601 string for datetime, JSON number for number (not a numeric string), array of strings for stringOptions/arrayOptions, boolean for boolean, and "" (empty string) for null + "key": string // Required only for stringObject (the metadata key to filter on) } ] ``` @@ -208,7 +206,7 @@ def get_many( ### Structured Data - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + - `metadata` (stringObject) - Metadata key-value pairs. Use the `key` parameter to filter on a specific metadata key. Metadata values are stored as strings, so match numbers and booleans by their string form with `=` (for example `"value": "true"` or `"value": "42"`); numeric comparisons such as `>` are not supported on metadata. The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. @@ -489,21 +487,19 @@ async def get_many( ```json [ { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "arrayOptions", "stringObject", "boolean", "null" "column": string, // Required. Column to filter on (see available columns below) "operator": string, // Required. Operator based on type: // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "is not empty", "matches" (input/output only) + // - stringOptions: "any of", "none of" (works on any string column except input/output) // - arrayOptions: "any of", "none of", "all of" // - number: "=", ">", "<", ">=", "<=" // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" // - boolean: "=", "<>" // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + "value": any, // Required. Type depends on filter type: ISO 8601 string for datetime, JSON number for number (not a numeric string), array of strings for stringOptions/arrayOptions, boolean for boolean, and "" (empty string) for null + "key": string // Required only for stringObject (the metadata key to filter on) } ] ``` @@ -553,7 +549,7 @@ async def get_many( ### Structured Data - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + - `metadata` (stringObject) - Metadata key-value pairs. Use the `key` parameter to filter on a specific metadata key. Metadata values are stored as strings, so match numbers and booleans by their string form with `=` (for example `"value": "true"` or `"value": "42"`); numeric comparisons such as `>` are not supported on metadata. The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. diff --git a/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py b/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py index df623c2fa..f52016865 100644 --- a/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py +++ b/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py @@ -17,7 +17,7 @@ class DashboardWidgetFilter(UniversalBaseModel): | `number` | number | `=`, `>`, `<`, `>=`, `<=` | | `datetime` | ISO datetime string | `>`, `<`, `>=`, `<=` | | `boolean` | boolean | `=`, `<>` | - | `null` | `""` | `is null`, `is not null` | + | `null` | omit, or `""` | `is null`, `is not null` | | `stringOptions` | list of strings | `any of`, `none of` | | `arrayOptions` | list of strings | `any of`, `none of`, `all of` | | `categoryOptions` | list of strings (requires `key`) | `any of`, `none of` | From 39ed50aaae1fba247d02a21c9bfb9603bd468b33 Mon Sep 17 00:00:00 2001 From: Niklas Semmler Date: Wed, 11 Nov 2026 09:58:41 +0100 Subject: [PATCH 2/2] fix(api): drop null filter value change until servers accept it Released servers v4.23.0 through v4.78.0 require `value: ""` on null filters, so the SDK must keep sending it. The spec is being reverted upstream in langfuse/langfuse#20059; restore the generated filter types to main. Co-Authored-By: Claude Opus 5.5 --- .../api/evaluation_commons/types/evaluation_rule_filter.py | 2 +- .../evaluation_commons/types/null_evaluation_rule_filter.py | 4 ++-- .../dashboard_widgets/types/dashboard_widget_filter.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py b/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py index 5e38561c7..160026a20 100644 --- a/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py +++ b/langfuse/api/evaluation_commons/types/evaluation_rule_filter.py @@ -647,7 +647,7 @@ class EvaluationRuleFilter_Null(UniversalBaseModel): type: typing.Literal["null"] = "null" column: str operator: EvaluationRuleNullFilterOperator - value: typing.Optional[typing.Literal[""]] = None + value: typing.Literal[""] = "" model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py b/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py index 8fafb3ca5..17343d87b 100644 --- a/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py +++ b/langfuse/api/evaluation_commons/types/null_evaluation_rule_filter.py @@ -14,9 +14,9 @@ class NullEvaluationRuleFilter(UniversalBaseModel): """ operator: EvaluationRuleNullFilterOperator - value: typing.Optional[typing.Literal[""]] = pydantic.Field(default=None) + value: typing.Literal[""] = pydantic.Field(default="") """ - Optional. Defaults to an empty string. + Required empty-string placeholder used by the runtime filter contract. """ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( diff --git a/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py b/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py index f52016865..df623c2fa 100644 --- a/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py +++ b/langfuse/api/unstable/dashboard_widgets/types/dashboard_widget_filter.py @@ -17,7 +17,7 @@ class DashboardWidgetFilter(UniversalBaseModel): | `number` | number | `=`, `>`, `<`, `>=`, `<=` | | `datetime` | ISO datetime string | `>`, `<`, `>=`, `<=` | | `boolean` | boolean | `=`, `<>` | - | `null` | omit, or `""` | `is null`, `is not null` | + | `null` | `""` | `is null`, `is not null` | | `stringOptions` | list of strings | `any of`, `none of` | | `arrayOptions` | list of strings | `any of`, `none of`, `all of` | | `categoryOptions` | list of strings (requires `key`) | `any of`, `none of` |