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
3 changes: 3 additions & 0 deletions langfuse/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
BlobStorageExportFrequency,
BlobStorageExportMode,
BlobStorageExportSource,
BlobStorageIntegrationAuthType,
BlobStorageIntegrationDeletionResponse,
BlobStorageIntegrationFileType,
BlobStorageIntegrationFileTypeResponse,
Expand Down Expand Up @@ -424,6 +425,7 @@
"BlobStorageExportFrequency": ".blob_storage_integrations",
"BlobStorageExportMode": ".blob_storage_integrations",
"BlobStorageExportSource": ".blob_storage_integrations",
"BlobStorageIntegrationAuthType": ".blob_storage_integrations",
"BlobStorageIntegrationDeletionResponse": ".blob_storage_integrations",
"BlobStorageIntegrationFileType": ".blob_storage_integrations",
"BlobStorageIntegrationFileTypeResponse": ".blob_storage_integrations",
Expand Down Expand Up @@ -813,6 +815,7 @@ def __dir__():
"BlobStorageExportFrequency",
"BlobStorageExportMode",
"BlobStorageExportSource",
"BlobStorageIntegrationAuthType",
"BlobStorageIntegrationDeletionResponse",
"BlobStorageIntegrationFileType",
"BlobStorageIntegrationFileTypeResponse",
Expand Down
3 changes: 3 additions & 0 deletions langfuse/api/blob_storage_integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
BlobStorageExportFrequency,
BlobStorageExportMode,
BlobStorageExportSource,
BlobStorageIntegrationAuthType,
BlobStorageIntegrationDeletionResponse,
BlobStorageIntegrationFileType,
BlobStorageIntegrationFileTypeResponse,
Expand All @@ -26,6 +27,7 @@
"BlobStorageExportFrequency": ".types",
"BlobStorageExportMode": ".types",
"BlobStorageExportSource": ".types",
"BlobStorageIntegrationAuthType": ".types",
"BlobStorageIntegrationDeletionResponse": ".types",
"BlobStorageIntegrationFileType": ".types",
"BlobStorageIntegrationFileTypeResponse": ".types",
Expand Down Expand Up @@ -70,6 +72,7 @@ def __dir__():
"BlobStorageExportFrequency",
"BlobStorageExportMode",
"BlobStorageExportSource",
"BlobStorageIntegrationAuthType",
"BlobStorageIntegrationDeletionResponse",
"BlobStorageIntegrationFileType",
"BlobStorageIntegrationFileTypeResponse",
Expand Down
29 changes: 27 additions & 2 deletions langfuse/api/blob_storage_integrations/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .types.blob_storage_export_frequency import BlobStorageExportFrequency
from .types.blob_storage_export_mode import BlobStorageExportMode
from .types.blob_storage_export_source import BlobStorageExportSource
from .types.blob_storage_integration_auth_type import BlobStorageIntegrationAuthType
from .types.blob_storage_integration_deletion_response import (
BlobStorageIntegrationDeletionResponse,
)
Expand Down Expand Up @@ -92,6 +93,8 @@ def upsert_blob_storage_integration(
file_type: BlobStorageIntegrationFileType,
export_mode: BlobStorageExportMode,
endpoint: typing.Optional[str] = OMIT,
auth_type: typing.Optional[BlobStorageIntegrationAuthType] = OMIT,
role_arn: typing.Optional[str] = OMIT,
access_key_id: typing.Optional[str] = OMIT,
secret_access_key: typing.Optional[str] = OMIT,
prefix: typing.Optional[str] = OMIT,
Expand Down Expand Up @@ -134,8 +137,16 @@ def upsert_blob_storage_integration(
endpoint : typing.Optional[str]
Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE.

auth_type : typing.Optional[BlobStorageIntegrationAuthType]
Authentication method. `ROLE_ARN` requires type `S3`, a concrete AWS region (not `auto`), and `roleArn`; `accessKeyId` and `secretAccessKey` must then be omitted, and `endpoint` and `forcePathStyle` are ignored. When omitted, `ROLE_ARN` applies to type `S3` if the request sends `roleArn` or the integration already uses `ROLE_ARN`, unless the request sends `accessKeyId` or `secretAccessKey`; otherwise `ACCESS_KEYS` applies.

The external ID is generated on the first save that uses `ROLE_ARN` and stays the same afterwards. That save stores the integration with `enabled: false`, because the trust policy cannot reference the new ID yet. Add the trust policy for `crossAccountRoleArn` with the returned `externalId` to your role, then update the integration with `enabled: true`.

role_arn : typing.Optional[str]
ARN of the IAM role in your AWS account that Langfuse assumes when `authType` is `ROLE_ARN`, e.g. `arn:aws:iam::123456789012:role/langfuse-export`. When omitted on update, the stored role ARN is kept.

access_key_id : typing.Optional[str]
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE.
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE or when `authType` is `ROLE_ARN`.

secret_access_key : typing.Optional[str]
Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type.
Expand Down Expand Up @@ -211,6 +222,8 @@ def upsert_blob_storage_integration(
file_type=file_type,
export_mode=export_mode,
endpoint=endpoint,
auth_type=auth_type,
role_arn=role_arn,
access_key_id=access_key_id,
secret_access_key=secret_access_key,
prefix=prefix,
Expand Down Expand Up @@ -371,6 +384,8 @@ async def upsert_blob_storage_integration(
file_type: BlobStorageIntegrationFileType,
export_mode: BlobStorageExportMode,
endpoint: typing.Optional[str] = OMIT,
auth_type: typing.Optional[BlobStorageIntegrationAuthType] = OMIT,
role_arn: typing.Optional[str] = OMIT,
access_key_id: typing.Optional[str] = OMIT,
secret_access_key: typing.Optional[str] = OMIT,
prefix: typing.Optional[str] = OMIT,
Expand Down Expand Up @@ -413,8 +428,16 @@ async def upsert_blob_storage_integration(
endpoint : typing.Optional[str]
Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE.

auth_type : typing.Optional[BlobStorageIntegrationAuthType]
Authentication method. `ROLE_ARN` requires type `S3`, a concrete AWS region (not `auto`), and `roleArn`; `accessKeyId` and `secretAccessKey` must then be omitted, and `endpoint` and `forcePathStyle` are ignored. When omitted, `ROLE_ARN` applies to type `S3` if the request sends `roleArn` or the integration already uses `ROLE_ARN`, unless the request sends `accessKeyId` or `secretAccessKey`; otherwise `ACCESS_KEYS` applies.

The external ID is generated on the first save that uses `ROLE_ARN` and stays the same afterwards. That save stores the integration with `enabled: false`, because the trust policy cannot reference the new ID yet. Add the trust policy for `crossAccountRoleArn` with the returned `externalId` to your role, then update the integration with `enabled: true`.

role_arn : typing.Optional[str]
ARN of the IAM role in your AWS account that Langfuse assumes when `authType` is `ROLE_ARN`, e.g. `arn:aws:iam::123456789012:role/langfuse-export`. When omitted on update, the stored role ARN is kept.

access_key_id : typing.Optional[str]
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE.
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE or when `authType` is `ROLE_ARN`.

secret_access_key : typing.Optional[str]
Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type.
Expand Down Expand Up @@ -498,6 +521,8 @@ async def main() -> None:
file_type=file_type,
export_mode=export_mode,
endpoint=endpoint,
auth_type=auth_type,
role_arn=role_arn,
access_key_id=access_key_id,
secret_access_key=secret_access_key,
prefix=prefix,
Expand Down
29 changes: 27 additions & 2 deletions langfuse/api/blob_storage_integrations/raw_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .types.blob_storage_export_frequency import BlobStorageExportFrequency
from .types.blob_storage_export_mode import BlobStorageExportMode
from .types.blob_storage_export_source import BlobStorageExportSource
from .types.blob_storage_integration_auth_type import BlobStorageIntegrationAuthType
from .types.blob_storage_integration_deletion_response import (
BlobStorageIntegrationDeletionResponse,
)
Expand Down Expand Up @@ -149,6 +150,8 @@ def upsert_blob_storage_integration(
file_type: BlobStorageIntegrationFileType,
export_mode: BlobStorageExportMode,
endpoint: typing.Optional[str] = OMIT,
auth_type: typing.Optional[BlobStorageIntegrationAuthType] = OMIT,
role_arn: typing.Optional[str] = OMIT,
access_key_id: typing.Optional[str] = OMIT,
secret_access_key: typing.Optional[str] = OMIT,
prefix: typing.Optional[str] = OMIT,
Expand Down Expand Up @@ -191,8 +194,16 @@ def upsert_blob_storage_integration(
endpoint : typing.Optional[str]
Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE.

auth_type : typing.Optional[BlobStorageIntegrationAuthType]
Authentication method. `ROLE_ARN` requires type `S3`, a concrete AWS region (not `auto`), and `roleArn`; `accessKeyId` and `secretAccessKey` must then be omitted, and `endpoint` and `forcePathStyle` are ignored. When omitted, `ROLE_ARN` applies to type `S3` if the request sends `roleArn` or the integration already uses `ROLE_ARN`, unless the request sends `accessKeyId` or `secretAccessKey`; otherwise `ACCESS_KEYS` applies.

The external ID is generated on the first save that uses `ROLE_ARN` and stays the same afterwards. That save stores the integration with `enabled: false`, because the trust policy cannot reference the new ID yet. Add the trust policy for `crossAccountRoleArn` with the returned `externalId` to your role, then update the integration with `enabled: true`.

role_arn : typing.Optional[str]
ARN of the IAM role in your AWS account that Langfuse assumes when `authType` is `ROLE_ARN`, e.g. `arn:aws:iam::123456789012:role/langfuse-export`. When omitted on update, the stored role ARN is kept.

access_key_id : typing.Optional[str]
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE.
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE or when `authType` is `ROLE_ARN`.

secret_access_key : typing.Optional[str]
Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type.
Expand Down Expand Up @@ -236,6 +247,8 @@ def upsert_blob_storage_integration(
"bucketName": bucket_name,
"endpoint": endpoint,
"region": region,
"authType": auth_type,
"roleArn": role_arn,
"accessKeyId": access_key_id,
"secretAccessKey": secret_access_key,
"prefix": prefix,
Expand Down Expand Up @@ -646,6 +659,8 @@ async def upsert_blob_storage_integration(
file_type: BlobStorageIntegrationFileType,
export_mode: BlobStorageExportMode,
endpoint: typing.Optional[str] = OMIT,
auth_type: typing.Optional[BlobStorageIntegrationAuthType] = OMIT,
role_arn: typing.Optional[str] = OMIT,
access_key_id: typing.Optional[str] = OMIT,
secret_access_key: typing.Optional[str] = OMIT,
prefix: typing.Optional[str] = OMIT,
Expand Down Expand Up @@ -688,8 +703,16 @@ async def upsert_blob_storage_integration(
endpoint : typing.Optional[str]
Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE.

auth_type : typing.Optional[BlobStorageIntegrationAuthType]
Authentication method. `ROLE_ARN` requires type `S3`, a concrete AWS region (not `auto`), and `roleArn`; `accessKeyId` and `secretAccessKey` must then be omitted, and `endpoint` and `forcePathStyle` are ignored. When omitted, `ROLE_ARN` applies to type `S3` if the request sends `roleArn` or the integration already uses `ROLE_ARN`, unless the request sends `accessKeyId` or `secretAccessKey`; otherwise `ACCESS_KEYS` applies.

The external ID is generated on the first save that uses `ROLE_ARN` and stays the same afterwards. That save stores the integration with `enabled: false`, because the trust policy cannot reference the new ID yet. Add the trust policy for `crossAccountRoleArn` with the returned `externalId` to your role, then update the integration with `enabled: true`.

role_arn : typing.Optional[str]
ARN of the IAM role in your AWS account that Langfuse assumes when `authType` is `ROLE_ARN`, e.g. `arn:aws:iam::123456789012:role/langfuse-export`. When omitted on update, the stored role ARN is kept.

access_key_id : typing.Optional[str]
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE.
Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE or when `authType` is `ROLE_ARN`.

secret_access_key : typing.Optional[str]
Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type.
Expand Down Expand Up @@ -733,6 +756,8 @@ async def upsert_blob_storage_integration(
"bucketName": bucket_name,
"endpoint": endpoint,
"region": region,
"authType": auth_type,
"roleArn": role_arn,
"accessKeyId": access_key_id,
"secretAccessKey": secret_access_key,
"prefix": prefix,
Expand Down
3 changes: 3 additions & 0 deletions langfuse/api/blob_storage_integrations/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .blob_storage_export_frequency import BlobStorageExportFrequency
from .blob_storage_export_mode import BlobStorageExportMode
from .blob_storage_export_source import BlobStorageExportSource
from .blob_storage_integration_auth_type import BlobStorageIntegrationAuthType
from .blob_storage_integration_deletion_response import (
BlobStorageIntegrationDeletionResponse,
)
Expand All @@ -32,6 +33,7 @@
"BlobStorageExportFrequency": ".blob_storage_export_frequency",
"BlobStorageExportMode": ".blob_storage_export_mode",
"BlobStorageExportSource": ".blob_storage_export_source",
"BlobStorageIntegrationAuthType": ".blob_storage_integration_auth_type",
"BlobStorageIntegrationDeletionResponse": ".blob_storage_integration_deletion_response",
"BlobStorageIntegrationFileType": ".blob_storage_integration_file_type",
"BlobStorageIntegrationFileTypeResponse": ".blob_storage_integration_file_type_response",
Expand Down Expand Up @@ -76,6 +78,7 @@ def __dir__():
"BlobStorageExportFrequency",
"BlobStorageExportMode",
"BlobStorageExportSource",
"BlobStorageIntegrationAuthType",
"BlobStorageIntegrationDeletionResponse",
"BlobStorageIntegrationFileType",
"BlobStorageIntegrationFileTypeResponse",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This file was auto-generated by Fern from our API Definition.

import typing

from ...core import enum

T_Result = typing.TypeVar("T_Result")


class BlobStorageIntegrationAuthType(enum.StrEnum):
"""
How Langfuse authenticates to the bucket.
- `ACCESS_KEYS`: stored access key ID and secret access key.
- `ROLE_ARN`: Langfuse assumes an IAM role in your AWS account (Amazon S3 only). Langfuse's cross-account role (`crossAccountRoleArn`) assumes your role with the integration's `externalId`, using a session limited to writing under the configured prefix. Requires an Enterprise plan and a deployment that configures a cross-account role; `crossAccountRoleArn` is not null when both apply.
"""

ACCESS_KEYS = "ACCESS_KEYS"
ROLE_ARN = "ROLE_ARN"

def visit(
self,
access_keys: typing.Callable[[], T_Result],
role_arn: typing.Callable[[], T_Result],
) -> T_Result:
if self is BlobStorageIntegrationAuthType.ACCESS_KEYS:
return access_keys()
if self is BlobStorageIntegrationAuthType.ROLE_ARN:
return role_arn()
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .blob_storage_export_frequency import BlobStorageExportFrequency
from .blob_storage_export_mode import BlobStorageExportMode
from .blob_storage_export_source import BlobStorageExportSource
from .blob_storage_integration_auth_type import BlobStorageIntegrationAuthType
from .blob_storage_integration_file_type_response import (
BlobStorageIntegrationFileTypeResponse,
)
Expand All @@ -24,9 +25,33 @@ class BlobStorageIntegrationResponse(UniversalBaseModel):
bucket_name: typing_extensions.Annotated[str, FieldMetadata(alias="bucketName")]
endpoint: typing.Optional[str] = None
region: str
auth_type: typing_extensions.Annotated[
BlobStorageIntegrationAuthType, FieldMetadata(alias="authType")
]
Comment on lines +28 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Callers on a self-hosted Langfuse server older than the authType release would get a ValidationError from every blob storage get, list or upsert, where the base parsed fine. New response fields are declared required with no default. Fix: give every newly added response field a default (= None for auth_type, an empty list for rule_ids), which covers the 2 sites listed. Same pattern at 2 sites (blob_storage_integration_response.py:28, unstable/topics/types/topic_facet.py:22).

Why this was flagged

This only matters if a caller upgrades the SDK while pointing at a Langfuse server that does not yet send authType or ruleIds, such as a self-hosted deployment that predates langfuse/langfuse 9a75785. BlobStorageIntegrationResponse.auth_type (blob_storage_integration_response.py:28-30) is annotated BlobStorageIntegrationAuthType with no default, so it is required. The raw client decodes 2xx bodies with parse_obj_as (core/pydantic_utilities.py:53-60), which calls TypeAdapter.validate_python. A body without authType therefore raises a pydantic ValidationError instead of returning data. On the base branch the same responses parse, because the field did not exist. TopicFacet.rule_ids (topic_facet.py:22-24) is also required via pydantic.Field(), so list_facets/get_facet fail the same way against servers without ruleIds. Nothing else guards against the missing key.

Verification: blob_storage_integration_response.py adds auth_type with no = ... default, so the field is required. topic_facet.py adds rule_ids: ... = pydantic.Field(), which is also required. In the base file this field does not exist, so the base parsed these responses fine. parse_obj_as (core/pydantic_utilities.py:53-60) raises a pydantic ValidationError when a required field is missing.

Comment on lines +28 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Older server responses fail

If a self-hosted server predates authType, the newly required auth_type field makes parse_obj_as raise ValidationError on its response. Listing integrations fails, and an upsert can save successfully before the client raises an error—even when callers use only access keys.

Give the new response field a backward-compatible default so upgrading the SDK does not require upgrading the server.

Prompt To Fix With AI
This is a comment left during a code review.
Path: langfuse/api/blob_storage_integrations/types/blob_storage_integration_response.py
Line: 28-30

Comment:
**Older server responses fail**

If a self-hosted server predates `authType`, the newly required `auth_type` field makes `parse_obj_as` raise `ValidationError` on its response. Listing integrations fails, and an upsert can save successfully before the client raises an error—even when callers use only access keys.

Give the new response field a backward-compatible default so upgrading the SDK does not require upgrading the server.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

access_key_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="accessKeyId")
] = None
role_arn: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="roleArn")
] = pydantic.Field(default=None)
"""
IAM role Langfuse assumes. May be set while `authType` is `ACCESS_KEYS` if the integration used `ROLE_ARN` before.
"""

external_id: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="externalId")
] = pydantic.Field(default=None)
"""
External ID Langfuse passes when assuming `roleArn`. Require it in the role's trust policy with an `sts:ExternalId` condition. Null until the integration first uses `ROLE_ARN`.
"""

cross_account_role_arn: typing_extensions.Annotated[
typing.Optional[str], FieldMetadata(alias="crossAccountRoleArn")
] = pydantic.Field(default=None)
"""
Langfuse role that must be trusted by `roleArn`. Null when the deployment does not support IAM role auth or the organization is not on an Enterprise plan.
"""

prefix: str
export_frequency: typing_extensions.Annotated[
BlobStorageExportFrequency, FieldMetadata(alias="exportFrequency")
Expand Down
Loading
Loading