From f8635204c34baabac92003023099d8aaff154ca1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 29 Sep 2026 15:38:47 +0000 Subject: [PATCH 1/7] feat(otel): cap oversized export batches via LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES Co-authored-by: Hassieb Pakzad --- langfuse/_client/client.py | 2 +- langfuse/_client/environment_variables.py | 14 ++++ langfuse/_client/span_processor.py | 43 +++++++++- tests/unit/test_span_processor.py | 95 +++++++++++++++++++++++ 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index 42d861fd4..a56188ab3 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -268,7 +268,7 @@ def mask_otel_spans( additional_headers (Optional[Dict[str, str]]): Additional headers to include in all API requests and in the default OTLPSpanExporter requests. These headers will be merged with default headers. Note: If httpx_client is provided, additional_headers must be set directly on your custom httpx_client as well. If `span_exporter` is provided, these headers are not wired into that exporter and must be configured on the exporter instance directly. tracer_provider(Optional[TracerProvider]): OpenTelemetry TracerProvider to use for Langfuse. This can be useful to set to have disconnected tracing between Langfuse and other OpenTelemetry-span emitting libraries. Note: To track active spans, the context is still shared between TracerProviders. This may lead to broken trace trees. id_generator (Optional[IdGenerator]): OpenTelemetry ID generator to use when Langfuse creates its own TracerProvider. If omitted, the OpenTelemetry SDK default is used. If `tracer_provider` is provided, or an OpenTelemetry TracerProvider is already registered globally, configure the ID generator on that provider instead. - span_exporter (Optional[SpanExporter]): Custom OpenTelemetry span exporter for the Langfuse span processor. If omitted, Langfuse creates an OTLPSpanExporter pointed at the Langfuse OTLP endpoint. If provided, Langfuse does not wire `base_url`, exporter headers, exporter auth, or exporter timeout into it. Configure endpoint, headers, and timeout on the exporter instance directly. If you are sending spans to Langfuse v4 or using Langfuse Cloud Fast Preview, include `x-langfuse-ingestion-version=4` on the exporter to enable real time processing of exported spans. + span_exporter (Optional[SpanExporter]): Custom OpenTelemetry span exporter for the Langfuse span processor. If omitted, Langfuse creates an OTLPSpanExporter pointed at the Langfuse OTLP endpoint. If provided, Langfuse does not wire `base_url`, exporter headers, exporter auth, exporter timeout, or the `LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES` request size limit into it. Configure endpoint, headers, and timeout on the exporter instance directly. If you are sending spans to Langfuse v4 or using Langfuse Cloud Fast Preview, include `x-langfuse-ingestion-version=4` on the exporter to enable real time processing of exported spans. Example: ```python diff --git a/langfuse/_client/environment_variables.py b/langfuse/_client/environment_variables.py index 0a95d53ba..3b95d383f 100644 --- a/langfuse/_client/environment_variables.py +++ b/langfuse/_client/environment_variables.py @@ -62,6 +62,20 @@ **Default value:** ``/api/public/otel/v1/traces`` """ +LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES = "LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES" +""" +.. envvar:: LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES + +Maximum serialized size in bytes (before compression) of a span batch exported by the +default OTLP exporter. Oversized batches are dropped before sending and logged as a +failed export. Must be a positive integer. Custom span exporters are not affected. + +Requires ``opentelemetry-exporter-otlp-proto-http>=1.45.0``; older versions do not +enforce a limit. + +**Default value:** ``67108864`` (64 MiB) +""" + LANGFUSE_DEBUG = "LANGFUSE_DEBUG" """ .. envvar:: LANGFUSE_DEBUG diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index 9107ae7fc..f71ae77bf 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -12,10 +12,11 @@ """ import base64 +import inspect import logging import os import threading -from typing import Callable, Dict, List, Optional, cast +from typing import Any, Callable, Dict, List, Optional, cast from opentelemetry import context as context_api from opentelemetry.context import Context @@ -28,6 +29,7 @@ from langfuse._client.environment_variables import ( LANGFUSE_FLUSH_AT, LANGFUSE_FLUSH_INTERVAL, + LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, LANGFUSE_OTEL_TRACES_EXPORT_PATH, ) from langfuse._client.propagation import ( @@ -47,6 +49,32 @@ from langfuse.types import MaskOtelSpansFunction +def _resolve_max_batch_size_bytes() -> Optional[int]: + """Return the configured batch byte limit, or None to keep the exporter default.""" + raw_value = os.environ.get(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, "").strip() + if not raw_value: + return None + + if raw_value.isascii() and raw_value.isdigit() and int(raw_value) > 0: + return int(raw_value) + + langfuse_logger.warning( + "Invalid %s=%r. Expected a positive integer. Using the default limit.", + LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, + raw_value, + ) + return None + + +def _otlp_exporter_supports_max_request_size() -> bool: + try: + parameters = inspect.signature(OTLPSpanExporter.__init__).parameters + except (TypeError, ValueError): + return False + + return "max_request_size" in parameters + + class LangfuseSpanProcessor(BatchSpanProcessor): """OpenTelemetry span processor that exports spans to the Langfuse API. @@ -123,10 +151,23 @@ def __init__( else f"{base_url}/api/public/otel/v1/traces" ) + exporter_kwargs: Dict[str, Any] = {} + max_request_size = _resolve_max_batch_size_bytes() + if max_request_size is not None: + if _otlp_exporter_supports_max_request_size(): + exporter_kwargs["max_request_size"] = max_request_size + else: + langfuse_logger.warning( + "%s is set but not enforced. It requires " + "opentelemetry-exporter-otlp-proto-http>=1.45.0.", + LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, + ) + span_exporter = OTLPSpanExporter( endpoint=endpoint, headers=headers, timeout=timeout, + **exporter_kwargs, ) if media_manager is not None or mask_otel_spans is not None: diff --git a/tests/unit/test_span_processor.py b/tests/unit/test_span_processor.py index f16a59926..ace4cac6e 100644 --- a/tests/unit/test_span_processor.py +++ b/tests/unit/test_span_processor.py @@ -10,6 +10,7 @@ from langfuse._client.environment_variables import ( LANGFUSE_FLUSH_AT, LANGFUSE_FLUSH_INTERVAL, + LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, ) from langfuse._client.span_processor import LangfuseSpanProcessor @@ -60,6 +61,100 @@ def test_span_processor_uses_env_flush_settings_when_constructor_omits_them( processor.shutdown() +class RecordingOTLPSpanExporter(NoOpSpanExporter): + init_kwargs: dict = {} + + def __init__(self, *, endpoint=None, headers=None, timeout=None, **kwargs): + type(self).init_kwargs = kwargs + + +class RecordingOTLPSpanExporterWithRequestLimit(RecordingOTLPSpanExporter): + def __init__( + self, + *, + endpoint=None, + headers=None, + timeout=None, + max_request_size=None, + ): + super().__init__(max_request_size=max_request_size) + + +def _build_default_exporter_processor(monkeypatch, exporter_class): + exporter_class.init_kwargs = {} + monkeypatch.setattr(span_processor_module, "OTLPSpanExporter", exporter_class) + return LangfuseSpanProcessor( + public_key="pk-test", + secret_key="sk-test", + base_url="http://localhost:3000", + ) + + +def test_default_exporter_receives_configured_max_batch_size_bytes(monkeypatch): + monkeypatch.setenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, " 1024 ") + processor = _build_default_exporter_processor( + monkeypatch, RecordingOTLPSpanExporterWithRequestLimit + ) + + try: + assert RecordingOTLPSpanExporterWithRequestLimit.init_kwargs == { + "max_request_size": 1024 + } + finally: + processor.shutdown() + + +def test_default_exporter_keeps_upstream_limit_when_env_unset(monkeypatch): + monkeypatch.delenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, raising=False) + processor = _build_default_exporter_processor( + monkeypatch, RecordingOTLPSpanExporterWithRequestLimit + ) + + try: + assert RecordingOTLPSpanExporterWithRequestLimit.init_kwargs == { + "max_request_size": None + } + finally: + processor.shutdown() + + +@pytest.mark.parametrize("raw_value", ["0", "-5", "abc", "1.5"]) +def test_invalid_max_batch_size_bytes_falls_back_to_upstream_limit( + monkeypatch, caplog, raw_value +): + monkeypatch.setenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, raw_value) + + with caplog.at_level(logging.WARNING, logger="langfuse"): + processor = _build_default_exporter_processor( + monkeypatch, RecordingOTLPSpanExporterWithRequestLimit + ) + + try: + assert RecordingOTLPSpanExporterWithRequestLimit.init_kwargs == { + "max_request_size": None + } + assert LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES in caplog.text + finally: + processor.shutdown() + + +def test_max_batch_size_bytes_warns_when_exporter_lacks_request_limit( + monkeypatch, caplog +): + monkeypatch.setenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, "1024") + + with caplog.at_level(logging.WARNING, logger="langfuse"): + processor = _build_default_exporter_processor( + monkeypatch, RecordingOTLPSpanExporter + ) + + try: + assert RecordingOTLPSpanExporter.init_kwargs == {} + assert "opentelemetry-exporter-otlp-proto-http>=1.45.0" in caplog.text + finally: + processor.shutdown() + + @pytest.fixture def tracer_with_processor(): processor = LangfuseSpanProcessor( From 24a44ff1dd0854574c00e41f5d5bc162f1802693 Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:18:25 +0200 Subject: [PATCH 2/7] push --- langfuse/_client/span_processor.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index f71ae77bf..c3bf477ee 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -65,16 +65,6 @@ def _resolve_max_batch_size_bytes() -> Optional[int]: ) return None - -def _otlp_exporter_supports_max_request_size() -> bool: - try: - parameters = inspect.signature(OTLPSpanExporter.__init__).parameters - except (TypeError, ValueError): - return False - - return "max_request_size" in parameters - - class LangfuseSpanProcessor(BatchSpanProcessor): """OpenTelemetry span processor that exports spans to the Langfuse API. @@ -151,23 +141,11 @@ def __init__( else f"{base_url}/api/public/otel/v1/traces" ) - exporter_kwargs: Dict[str, Any] = {} - max_request_size = _resolve_max_batch_size_bytes() - if max_request_size is not None: - if _otlp_exporter_supports_max_request_size(): - exporter_kwargs["max_request_size"] = max_request_size - else: - langfuse_logger.warning( - "%s is set but not enforced. It requires " - "opentelemetry-exporter-otlp-proto-http>=1.45.0.", - LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, - ) - span_exporter = OTLPSpanExporter( endpoint=endpoint, headers=headers, timeout=timeout, - **exporter_kwargs, + max_request_size=_resolve_max_batch_size_bytes() ) if media_manager is not None or mask_otel_spans is not None: From c09b1444ced20cdaa44a5bc17ff47586406a8f15 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 30 Sep 2026 09:22:45 +0000 Subject: [PATCH 3/7] build(deps): require OpenTelemetry 1.45 for the OTLP request size cap Co-authored-by: Hassieb Pakzad --- langfuse/_client/span_exporter.py | 2 +- langfuse/types.py | 17 ++- pyproject.toml | 8 +- tests/unit/test_additional_headers_simple.py | 18 +-- uv.lock | 124 +++++++++++-------- 5 files changed, 102 insertions(+), 67 deletions(-) diff --git a/langfuse/_client/span_exporter.py b/langfuse/_client/span_exporter.py index 7ea56f81c..661a1a26c 100644 --- a/langfuse/_client/span_exporter.py +++ b/langfuse/_client/span_exporter.py @@ -9,7 +9,6 @@ from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.sdk.util import BoundedList from opentelemetry.trace import format_span_id, format_trace_id -from opentelemetry.util.types import AttributeValue from langfuse._client.attributes import LangfuseOtelSpanAttributes from langfuse._task_manager.media_manager import MediaManager @@ -17,6 +16,7 @@ from langfuse.logger import langfuse_logger from langfuse.media import LangfuseMedia from langfuse.types import ( + AttributeValue, MaskOtelSpansFunction, MaskOtelSpansParams, MaskOtelSpansResult, diff --git a/langfuse/types.py b/langfuse/types.py index 357438d56..66bddbca6 100644 --- a/langfuse/types.py +++ b/langfuse/types.py @@ -28,10 +28,9 @@ def my_evaluator(*, output: str, **kwargs) -> Evaluation: Protocol, Sequence, TypedDict, + Union, ) -from opentelemetry.util.types import AttributeValue - try: from typing import NotRequired # type: ignore except ImportError: @@ -40,6 +39,20 @@ def my_evaluator(*, output: str, **kwargs) -> Evaluation: from langfuse.api import MediaContentType +# Span attribute values accepted by the OpenTelemetry trace API. OpenTelemetry 1.45 +# widened ``opentelemetry.util.types.AttributeValue`` to the recursive ``AnyValue``, +# which mypy cannot use as a type alias. +AttributeValue = Union[ + str, + bool, + int, + float, + Sequence[str], + Sequence[bool], + Sequence[int], + Sequence[float], +] + SpanLevel = Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"] ScoreDataType = Literal["NUMERIC", "CATEGORICAL", "BOOLEAN", "TEXT", "CORRECTION"] diff --git a/pyproject.toml b/pyproject.toml index ab7c9170c..80ecefd23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,9 @@ dependencies = [ "backoff>=1.10.0", "wrapt>=1.14,<3", "packaging>=23.2,<27.0", - "opentelemetry-api>=1.33.1,<2", - "opentelemetry-sdk>=1.33.1,<2", - "opentelemetry-exporter-otlp-proto-http>=1.33.1,<2", + "opentelemetry-api>=1.45.0,<2", + "opentelemetry-sdk>=1.45.0,<2", + "opentelemetry-exporter-otlp-proto-http>=1.45.0,<2", "typing-extensions>=4.12.2,<5", ] @@ -71,6 +71,8 @@ build-backend = "uv_build" [tool.uv] # Basic protection against supply chain attacks exclude-newer = "7 days" +# OpenTelemetry 1.45 ships the OTLP request size cap the SDK relies on +exclude-newer-package = { "opentelemetry-api" = "3 days", "opentelemetry-sdk" = "3 days", "opentelemetry-semantic-conventions" = "3 days", "opentelemetry-proto" = "3 days", "opentelemetry-exporter-otlp-proto-common" = "3 days", "opentelemetry-exporter-otlp-proto-http" = "3 days", "opentelemetry-exporter-otlp-common" = "3 days", "opentelemetry-exporter-http-transport" = "3 days", "opentelemetry-instrumentation" = "3 days", "opentelemetry-instrumentation-threading" = "3 days" } [tool.uv.build-backend] module-root = "" diff --git a/tests/unit/test_additional_headers_simple.py b/tests/unit/test_additional_headers_simple.py index dd843b35a..7837c5c3e 100644 --- a/tests/unit/test_additional_headers_simple.py +++ b/tests/unit/test_additional_headers_simple.py @@ -180,16 +180,16 @@ def test_span_processor_has_additional_headers_in_otel_exporter(self): exporter = processor.span_exporter # Verify additional headers are in the exporter's headers - assert exporter._headers["X-Custom-Trace-Header"] == "trace-value" - assert exporter._headers["X-Override-Default"] == "override-value" + assert exporter._client._headers["x-custom-trace-header"] == "trace-value" + assert exporter._client._headers["x-override-default"] == "override-value" # Verify default headers are still present - assert "Authorization" in exporter._headers - assert "x-langfuse-sdk-name" in exporter._headers - assert "x-langfuse-public-key" in exporter._headers + assert "authorization" in exporter._client._headers + assert "x-langfuse-sdk-name" in exporter._client._headers + assert "x-langfuse-public-key" in exporter._client._headers # Check that our override worked - assert exporter._headers["X-Override-Default"] == "override-value" + assert exporter._client._headers["x-override-default"] == "override-value" def test_span_processor_none_additional_headers_works(self): """Test that span processor works with None additional headers.""" @@ -207,9 +207,9 @@ def test_span_processor_none_additional_headers_works(self): exporter = processor.span_exporter # Verify default headers are present - assert "Authorization" in exporter._headers - assert "x-langfuse-sdk-name" in exporter._headers - assert "x-langfuse-public-key" in exporter._headers + assert "authorization" in exporter._client._headers + assert "x-langfuse-sdk-name" in exporter._client._headers + assert "x-langfuse-public-key" in exporter._client._headers def test_span_processor_uses_custom_span_exporter_when_provided(self): """Test that a custom exporter bypasses the default OTLP exporter construction.""" diff --git a/uv.lock b/uv.lock index 0556ffa83..cd25b16de 100644 --- a/uv.lock +++ b/uv.lock @@ -3,9 +3,21 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "2026-09-17T13:32:25.771088061Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P7D" +[options.exclude-newer-package] +opentelemetry-api = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-exporter-http-transport = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-exporter-otlp-common = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-exporter-otlp-proto-common = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-exporter-otlp-proto-http = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-instrumentation = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-instrumentation-threading = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-proto = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-sdk = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } +opentelemetry-semantic-conventions = { timestamp = "0001-01-01T00:00:00Z", span = "P3D" } + [[package]] name = "annotated-types" version = "0.7.0" @@ -235,7 +247,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -327,18 +339,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -594,9 +594,9 @@ docs = [ requires-dist = [ { name = "backoff", specifier = ">=1.10.0" }, { name = "httpx", specifier = ">=0.15.4,<1.0" }, - { name = "opentelemetry-api", specifier = ">=1.33.1,<2" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.33.1,<2" }, - { name = "opentelemetry-sdk", specifier = ">=1.33.1,<2" }, + { name = "opentelemetry-api", specifier = ">=1.45.0,<2" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.45.0,<2" }, + { name = "opentelemetry-sdk", specifier = ">=1.45.0,<2" }, { name = "packaging", specifier = ">=23.2,<27.0" }, { name = "pydantic", specifier = ">=2,<3" }, { name = "typing-extensions", specifier = ">=4.12.2,<5" }, @@ -954,50 +954,79 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.40.0" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, + { url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" }, +] + +[[package]] +name = "opentelemetry-exporter-http-transport" +version = "0.66b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/31/cbedb10e08c3c932b80f58edf055a16bb48a500c23c617b758fb3ec18f08/opentelemetry_exporter_http_transport-0.66b0.tar.gz", hash = "sha256:2c229b6593eaa22c86d9b8a15843dc23b406dbda00fb138339189aab07923b4e", size = 11694, upload-time = "2026-09-25T12:32:29.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/65/34d7106eb45acda40f016c5b31b313ffbbf4744acd5f099f29eaf2958b4d/opentelemetry_exporter_http_transport-0.66b0-py3-none-any.whl", hash = "sha256:c82689928a11505a2a0a26a04afe0ce06d270d3db984bc16c1a80ffa902db00e", size = 12156, upload-time = "2026-09-25T12:32:05.621Z" }, +] + +[package.optional-dependencies] +urllib3 = [ + { name = "urllib3" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-common" +version = "0.66b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/09/01239cdfe8a414d46ed625b68b6da92666ffd16d93cc6dadf89404b4bd85/opentelemetry_exporter_otlp_common-0.66b0.tar.gz", hash = "sha256:362268ec6aa705e183776ff938539df1e8ce45bc5509d242538b1d40c26fe6a6", size = 14323, upload-time = "2026-09-25T12:32:31.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/a9/b6a7a9d40ba99ed71b18e17310339024b086578f5f32162df1c1c0cf6270/opentelemetry_exporter_otlp_common-0.66b0-py3-none-any.whl", hash = "sha256:35d24c867310f4713a9738b0202b6bade77955418fccb9e2f7eea01b76263916", size = 12387, upload-time = "2026-09-25T12:32:08.438Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.40.0" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/e0/ee3823dbdc10da15b5750becc37b61194dd7c55e3b56764ecbbe446f659a/opentelemetry_exporter_otlp_proto_common-1.45.0.tar.gz", hash = "sha256:36495115a0c6a7aa946cfda9d59b6ed4e917b6ab0f75cdaf66bc1b176ec1be1f", size = 18870, upload-time = "2026-09-25T12:32:34.141Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bf/3e21917368a81282ce9fc6d085a944dcaea6eed19de7d29cc979375db347/opentelemetry_exporter_otlp_proto_common-1.45.0-py3-none-any.whl", hash = "sha256:7e1410ae6d3ed301f7a74bec96467d519d3f37e52f9d95b309ae125e5a1af863", size = 15394, upload-time = "2026-09-25T12:32:12.663Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.40.0" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-http-transport", extra = ["urllib3"] }, + { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, - { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/78/a503801c1c8f80b1d8aad0e14106a7c57f39344001652a78a683f9a9089e/opentelemetry_exporter_otlp_proto_http-1.45.0.tar.gz", hash = "sha256:2f35496d96809f946f41b8805e6b93aec6c9b71b5fd759b75b6af4c084d992ae", size = 28818, upload-time = "2026-09-25T12:32:35.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/e7/18/875e0ee47e035f324e42eda9aab47d58381a4412181dca251d3504e24680/opentelemetry_exporter_otlp_proto_http-1.45.0-py3-none-any.whl", hash = "sha256:74385f99266dafdefff41f3da4b76f6dba79d237a9d4487e6ff1ee653c74a0a3", size = 22260, upload-time = "2026-09-25T12:32:14.658Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.61b0" +version = "0.66b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1005,62 +1034,62 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/97/e6959310cff3dbbce24ca0a60e5e12f773320d17480c779b038b55defd35/opentelemetry_instrumentation-0.66b0.tar.gz", hash = "sha256:316715ed7b33a12530c0ae3e8d4cdf26d70e1aad358d7ba256e5dea2d4ceed71", size = 43308, upload-time = "2026-09-25T12:35:46.504Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/4b/08/bf39ca469f178963715ff6fd7012eec0b438f472703b11a64a40cbb14f6f/opentelemetry_instrumentation-0.66b0-py3-none-any.whl", hash = "sha256:0b32e2fcfec1e4bd48511a3b77ce32bfb1c16dcd5479396b21f9086a4b06c0c9", size = 36900, upload-time = "2026-09-25T12:34:50.029Z" }, ] [[package]] name = "opentelemetry-instrumentation-threading" -version = "0.61b0" +version = "0.66b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/8f/8dedba66100cda58af057926449a5e58e6c008bec02bc2746c03c3d85dcd/opentelemetry_instrumentation_threading-0.61b0.tar.gz", hash = "sha256:38e0263c692d15a7a458b3fa0286d29290448fa4ac4c63045edac438c6113433", size = 9163, upload-time = "2026-03-04T14:20:50.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/ee/968d70b3c80c6f3bbdb168a3018cf18c78fbeadd330f6f4851fe029f8410/opentelemetry_instrumentation_threading-0.66b0.tar.gz", hash = "sha256:4c1c9adf42b83b6f44ca4bb99799e066ccfb1869c2029bc73d862c40a8ef932e", size = 9033, upload-time = "2026-09-25T12:36:17.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/77/c06d960aede1a014812aa4fafde0ae546d790f46416fbeafa2b32095aae3/opentelemetry_instrumentation_threading-0.61b0-py3-none-any.whl", hash = "sha256:735f4a1dc964202fc8aff475efc12bb64e6566f22dff52d5cb5de864b3fe1a70", size = 9337, upload-time = "2026-03-04T14:19:57.983Z" }, + { url = "https://files.pythonhosted.org/packages/37/24/0667079f02a507952baa6e14df90d3da870650fb029b6e1860a9e2dde3a9/opentelemetry_instrumentation_threading-0.66b0-py3-none-any.whl", hash = "sha256:4aaf38b96b47a04319b8d96bee4f1daa04e0d6c697525595746aeb61618c002d", size = 8491, upload-time = "2026-09-25T12:35:34.555Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.40.0" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/28/67c38cfb7e2bdfdd0cde7dcdd0424aed0291fbd64aa4ea3e7e913734711a/opentelemetry_proto-1.45.0.tar.gz", hash = "sha256:96ee414f24bc3f61ea8e17dc56b4348d4049d73db3eb17c6b3edf75b5b403300", size = 46489, upload-time = "2026-09-25T12:32:41.409Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, + { url = "https://files.pythonhosted.org/packages/6b/45/814d55714507c7ce9c9cd86a4f83ceb5a74ab0bffcc63ff493943f138de2/opentelemetry_proto-1.45.0-py3-none-any.whl", hash = "sha256:9731566359d7b8e1ee1e149d8e4ad6865bab965e657ad3387ec2784d16927666", size = 72486, upload-time = "2026-09-25T12:32:23.36Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.40.0" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/ed/ad32d76cc86ebce601105d01d13f8a08d6abc1852eaf69d79cf199bc1ee7/opentelemetry_sdk-1.45.0.tar.gz", hash = "sha256:20caa5130505e386c67c3da1c76e446c842698ced54c76c6148679539aa97972", size = 218337, upload-time = "2026-09-25T12:32:43.591Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ae/46963201ce58d6b86f9e54087a39720a35ac9f57c3a9e440d275995c2b02/opentelemetry_sdk-1.45.0-py3-none-any.whl", hash = "sha256:5dc634c946546f61b757c5b1781f9fd1a10e96ffaf7357c797e508fe9c57e75e", size = 140132, upload-time = "2026-09-25T12:32:25.399Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.61b0" +version = "0.66b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/21/910f085c0b83b80e45c341c7859baef37c877046b2c44c88d5ff2d5db948/opentelemetry_semantic_conventions-0.66b0.tar.gz", hash = "sha256:97a77dce484c54861e7eeff7651fd8a806dd3c30e501dc316730215ec36890e6", size = 150247, upload-time = "2026-09-25T12:32:44.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, + { url = "https://files.pythonhosted.org/packages/f4/93/2ad416d69c4ca8fd21ebc6894f23f4eda744dc6f43a14d7a9ffd7ed3abbd/opentelemetry_semantic_conventions-0.66b0-py3-none-any.whl", hash = "sha256:175b19dd98c4473f4f43a2b1df59186fd7b4a48cd3f77cbe03438b6d6fda230a", size = 206277, upload-time = "2026-09-25T12:32:26.484Z" }, ] [[package]] @@ -2358,15 +2387,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, ] -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] - [[package]] name = "zstandard" version = "0.25.0" From 7ecf497e5dd04edef000b9352fae554f929cff6c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 30 Sep 2026 09:22:46 +0000 Subject: [PATCH 4/7] feat(otel): always cap default exporter batches at 64 MiB Co-authored-by: Hassieb Pakzad --- langfuse/_client/environment_variables.py | 3 - langfuse/_client/span_processor.py | 6 +- tests/unit/test_span_processor.py | 168 +++++++++++++++------- 3 files changed, 116 insertions(+), 61 deletions(-) diff --git a/langfuse/_client/environment_variables.py b/langfuse/_client/environment_variables.py index 3b95d383f..424ba53fd 100644 --- a/langfuse/_client/environment_variables.py +++ b/langfuse/_client/environment_variables.py @@ -70,9 +70,6 @@ default OTLP exporter. Oversized batches are dropped before sending and logged as a failed export. Must be a positive integer. Custom span exporters are not affected. -Requires ``opentelemetry-exporter-otlp-proto-http>=1.45.0``; older versions do not -enforce a limit. - **Default value:** ``67108864`` (64 MiB) """ diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index c3bf477ee..524b2996d 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -12,11 +12,10 @@ """ import base64 -import inspect import logging import os import threading -from typing import Any, Callable, Dict, List, Optional, cast +from typing import Callable, Dict, List, Optional, cast from opentelemetry import context as context_api from opentelemetry.context import Context @@ -65,6 +64,7 @@ def _resolve_max_batch_size_bytes() -> Optional[int]: ) return None + class LangfuseSpanProcessor(BatchSpanProcessor): """OpenTelemetry span processor that exports spans to the Langfuse API. @@ -145,7 +145,7 @@ def __init__( endpoint=endpoint, headers=headers, timeout=timeout, - max_request_size=_resolve_max_batch_size_bytes() + max_request_size=_resolve_max_batch_size_bytes(), ) if media_manager is not None or mask_otel_spans is not None: diff --git a/tests/unit/test_span_processor.py b/tests/unit/test_span_processor.py index ace4cac6e..a123fd108 100644 --- a/tests/unit/test_span_processor.py +++ b/tests/unit/test_span_processor.py @@ -1,10 +1,20 @@ import logging -from typing import Sequence +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import List, Sequence from unittest.mock import patch import pytest +from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans from opentelemetry.sdk.trace import ReadableSpan, TracerProvider -from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.sdk.trace.export import ( + SimpleSpanProcessor, + SpanExporter, + SpanExportResult, +) +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) import langfuse._client.span_processor as span_processor_module from langfuse._client.environment_variables import ( @@ -61,96 +71,144 @@ def test_span_processor_uses_env_flush_settings_when_constructor_omits_them( processor.shutdown() -class RecordingOTLPSpanExporter(NoOpSpanExporter): - init_kwargs: dict = {} +class _RecordingOTLPHandler(BaseHTTPRequestHandler): + received_body_sizes: List[int] + + def do_POST(self): + body = self.rfile.read(int(self.headers.get("Content-Length", 0))) + self.received_body_sizes.append(len(body)) + self.send_response(200) + self.end_headers() + + def log_message(self, *args): + pass + + +@pytest.fixture +def otlp_http_server(): + received_body_sizes: List[int] = [] + handler = type( + "Handler", + (_RecordingOTLPHandler,), + {"received_body_sizes": received_body_sizes}, + ) + server = HTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + + yield f"http://127.0.0.1:{server.server_port}", received_body_sizes + + server.shutdown() + server.server_close() + + +def _finished_spans(payload: str) -> List[ReadableSpan]: + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + with provider.get_tracer("test").start_as_current_span("span") as span: + span.set_attribute("input", payload) + provider.shutdown() - def __init__(self, *, endpoint=None, headers=None, timeout=None, **kwargs): - type(self).init_kwargs = kwargs + return list(exporter.get_finished_spans()) -class RecordingOTLPSpanExporterWithRequestLimit(RecordingOTLPSpanExporter): - def __init__( - self, - *, - endpoint=None, - headers=None, - timeout=None, - max_request_size=None, - ): - super().__init__(max_request_size=max_request_size) +def _serialized_request_size(spans: List[ReadableSpan]) -> int: + return len(encode_spans(spans).SerializePartialToString()) -def _build_default_exporter_processor(monkeypatch, exporter_class): - exporter_class.init_kwargs = {} - monkeypatch.setattr(span_processor_module, "OTLPSpanExporter", exporter_class) +def _default_exporter_processor(base_url: str) -> LangfuseSpanProcessor: return LangfuseSpanProcessor( public_key="pk-test", secret_key="sk-test", - base_url="http://localhost:3000", + base_url=base_url, ) -def test_default_exporter_receives_configured_max_batch_size_bytes(monkeypatch): - monkeypatch.setenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, " 1024 ") - processor = _build_default_exporter_processor( - monkeypatch, RecordingOTLPSpanExporterWithRequestLimit +@pytest.mark.parametrize( + ("limit_offset", "expected_result"), + [ + (-1, SpanExportResult.FAILURE), + (0, SpanExportResult.SUCCESS), + (1, SpanExportResult.SUCCESS), + ], +) +def test_default_exporter_enforces_max_batch_size_bytes_at_boundary( + monkeypatch, otlp_http_server, limit_offset, expected_result +): + base_url, received_body_sizes = otlp_http_server + spans = _finished_spans("x" * 1_000) + request_size = _serialized_request_size(spans) + monkeypatch.setenv( + LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, str(request_size + limit_offset) ) + processor = _default_exporter_processor(base_url) try: - assert RecordingOTLPSpanExporterWithRequestLimit.init_kwargs == { - "max_request_size": 1024 - } + result = processor._batch_processor._exporter.export(spans) finally: processor.shutdown() + assert result == expected_result + expected_requests = ( + [] if expected_result == SpanExportResult.FAILURE else [request_size] + ) + assert received_body_sizes == expected_requests -def test_default_exporter_keeps_upstream_limit_when_env_unset(monkeypatch): - monkeypatch.delenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, raising=False) - processor = _build_default_exporter_processor( - monkeypatch, RecordingOTLPSpanExporterWithRequestLimit + +def test_oversized_batch_is_dropped_on_flush_without_blocking_later_batches( + monkeypatch, caplog, otlp_http_server +): + base_url, received_body_sizes = otlp_http_server + oversized_spans = _finished_spans("secret-payload" * 1_000) + small_spans = _finished_spans("ok") + monkeypatch.setenv( + LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, + str(_serialized_request_size(oversized_spans) - 1), ) + processor = _default_exporter_processor(base_url) try: - assert RecordingOTLPSpanExporterWithRequestLimit.init_kwargs == { - "max_request_size": None - } + with caplog.at_level(logging.WARNING): + super(LangfuseSpanProcessor, processor).on_end(oversized_spans[0]) + assert processor.force_flush() + + assert received_body_sizes == [] + assert "Dropping span batch" in caplog.text + assert "secret-payload" not in caplog.text + + super(LangfuseSpanProcessor, processor).on_end(small_spans[0]) + assert processor.force_flush() finally: processor.shutdown() + assert received_body_sizes == [_serialized_request_size(small_spans)] -@pytest.mark.parametrize("raw_value", ["0", "-5", "abc", "1.5"]) -def test_invalid_max_batch_size_bytes_falls_back_to_upstream_limit( - monkeypatch, caplog, raw_value -): - monkeypatch.setenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, raw_value) - with caplog.at_level(logging.WARNING, logger="langfuse"): - processor = _build_default_exporter_processor( - monkeypatch, RecordingOTLPSpanExporterWithRequestLimit - ) +def test_default_exporter_uses_64_mib_limit_when_env_unset(monkeypatch): + monkeypatch.delenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, raising=False) + processor = _default_exporter_processor("http://localhost:3000") try: - assert RecordingOTLPSpanExporterWithRequestLimit.init_kwargs == { - "max_request_size": None - } - assert LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES in caplog.text + exporter = processor._batch_processor._exporter + assert exporter._max_request_size == 64 * 1024 * 1024 finally: processor.shutdown() -def test_max_batch_size_bytes_warns_when_exporter_lacks_request_limit( - monkeypatch, caplog +@pytest.mark.parametrize("raw_value", ["0", "-5", "abc", "1.5"]) +def test_invalid_max_batch_size_bytes_falls_back_to_default_limit( + monkeypatch, caplog, raw_value ): - monkeypatch.setenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, "1024") + monkeypatch.setenv(LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES, raw_value) with caplog.at_level(logging.WARNING, logger="langfuse"): - processor = _build_default_exporter_processor( - monkeypatch, RecordingOTLPSpanExporter - ) + processor = _default_exporter_processor("http://localhost:3000") try: - assert RecordingOTLPSpanExporter.init_kwargs == {} - assert "opentelemetry-exporter-otlp-proto-http>=1.45.0" in caplog.text + exporter = processor._batch_processor._exporter + assert exporter._max_request_size == 64 * 1024 * 1024 + assert LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES in caplog.text finally: processor.shutdown() From 636ce19420d2c383d27a3c62f5f58ea9c04b639c Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:32:54 +0200 Subject: [PATCH 5/7] push --- tests/live_provider/test_langchain.py | 2 +- .../test_langchain_integration.py | 18 +++++++++--------- tests/live_provider/test_openai.py | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/live_provider/test_langchain.py b/tests/live_provider/test_langchain.py index 5d0fcb94e..1891d6db3 100644 --- a/tests/live_provider/test_langchain.py +++ b/tests/live_provider/test_langchain.py @@ -280,7 +280,7 @@ def test_openai_instruct_usage(): """ ) | OpenAI( - model="gpt-3.5-turbo-instruct", + model="gpt-4.1", temperature=0, callbacks=[lf_handler], max_retries=3, diff --git a/tests/live_provider/test_langchain_integration.py b/tests/live_provider/test_langchain_integration.py index edb5455c4..055320de2 100644 --- a/tests/live_provider/test_langchain_integration.py +++ b/tests/live_provider/test_langchain_integration.py @@ -77,7 +77,7 @@ def test_stream_chat_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) def test_stream_completions_models(model_name): name = f"test_stream_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -181,7 +181,7 @@ def test_invoke_chat_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) def test_invoke_in_completions_models(model_name): name = f"test_invoke_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -230,7 +230,7 @@ def test_invoke_in_completions_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) def test_batch_in_completions_models(model_name): name = f"test_batch_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -386,7 +386,7 @@ async def test_astream_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) async def test_astream_completions_models(model_name): name = f"test_astream_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -494,7 +494,7 @@ async def test_ainvoke_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) async def test_ainvoke_in_completions_models(model_name): name = f"test_ainvoke_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -598,7 +598,7 @@ def test_chains_batch_in_chat_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) def test_chains_batch_in_completions_models(model_name): name = f"test_chains_batch_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -704,7 +704,7 @@ async def test_chains_abatch_in_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) async def test_chains_abatch_in_completions_models(model_name): name = f"test_chains_abatch_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -809,7 +809,7 @@ async def test_chains_ainvoke_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) async def test_chains_ainvoke_completions_models(model_name): name = f"test_chains_ainvoke_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -927,7 +927,7 @@ async def test_chains_astream_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) +@pytest.mark.parametrize("model_name", ["gpt-4.1"]) async def test_chains_astream_completions_models(model_name): name = f"test_chains_astream_completions_models-{create_uuid()}" tags = ["Hello", "world"] diff --git a/tests/live_provider/test_openai.py b/tests/live_provider/test_openai.py index b0294fecd..8aee8b00e 100644 --- a/tests/live_provider/test_openai.py +++ b/tests/live_provider/test_openai.py @@ -409,7 +409,7 @@ def test_openai_completion(openai): generation_name = create_uuid() completion = openai.OpenAI().completions.create( name=generation_name, - model="gpt-3.5-turbo-instruct", + model="gpt-4.1", prompt="1 + 1 = ", temperature=0, metadata={"someKey": "someResponse"}, @@ -428,7 +428,7 @@ def test_openai_completion(openai): assert completion.choices[0].text == generation.data[0].output assert generation.data[0].input == "1 + 1 = " assert generation.data[0].type == "GENERATION" - assert "gpt-3.5-turbo-instruct" in generation.data[0].model + assert "gpt-4.1" in generation.data[0].model assert generation.data[0].start_time is not None assert generation.data[0].end_time is not None assert generation.data[0].start_time < generation.data[0].end_time @@ -449,7 +449,7 @@ def test_openai_completion_stream(openai): generation_name = create_uuid() completion = openai.OpenAI().completions.create( name=generation_name, - model="gpt-3.5-turbo-instruct", + model="gpt-4.1", prompt="1 + 1 = ", temperature=0, metadata={"someKey": "someResponse"}, @@ -475,7 +475,7 @@ def test_openai_completion_stream(openai): assert generation.data[0].input == "1 + 1 = " assert generation.data[0].type == "GENERATION" - assert "gpt-3.5-turbo-instruct" in generation.data[0].model + assert "gpt-4.1" in generation.data[0].model assert generation.data[0].start_time is not None assert generation.data[0].end_time is not None assert generation.data[0].start_time < generation.data[0].end_time @@ -597,7 +597,7 @@ def test_openai_completion_with_langfuse_prompt(openai): ) openai.OpenAI().completions.create( name=generation_name, - model="gpt-3.5-turbo-instruct", + model="gpt-4.1", prompt="1 + 1 = ", temperature=0, metadata={"someKey": "someResponse"}, @@ -619,7 +619,7 @@ def test_fails_wrong_name(openai): with pytest.raises(TypeError, match="name must be a string"): openai.OpenAI().completions.create( name={"key": "generation_name"}, - model="gpt-3.5-turbo-instruct", + model="gpt-4.1", prompt="1 + 1 = ", temperature=0, ) @@ -629,7 +629,7 @@ def test_fails_wrong_trace_id(openai): with pytest.raises(TypeError, match="trace_id must be a string"): openai.OpenAI().completions.create( trace_id={"trace_id": "metadata"}, - model="gpt-3.5-turbo-instruct", + model="gpt-4.1", prompt="1 + 1 = ", temperature=0, ) From 9acf98afc4ee6985bb2e32e36f32c418eff81550 Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:43:02 +0200 Subject: [PATCH 6/7] push --- tests/live_provider/test_openai.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/live_provider/test_openai.py b/tests/live_provider/test_openai.py index 8aee8b00e..96bad1423 100644 --- a/tests/live_provider/test_openai.py +++ b/tests/live_provider/test_openai.py @@ -407,7 +407,7 @@ def test_openai_chat_completion_with_seed(openai): def test_openai_completion(openai): generation_name = create_uuid() - completion = openai.OpenAI().completions.create( + completion = openai.OpenAI().chat.completions.create( name=generation_name, model="gpt-4.1", prompt="1 + 1 = ", @@ -447,7 +447,7 @@ def test_openai_completion(openai): def test_openai_completion_stream(openai): generation_name = create_uuid() - completion = openai.OpenAI().completions.create( + completion = openai.OpenAI().chat.completions.create( name=generation_name, model="gpt-4.1", prompt="1 + 1 = ", @@ -504,7 +504,7 @@ def test_openai_completion_fail(openai): openai.api_key = "" with pytest.raises(Exception): - openai.OpenAI().completions.create( + openai.OpenAI().chat.completions.create( name=generation_name, model="fake", prompt="1 + 1 = ", @@ -546,7 +546,7 @@ def test_openai_completion_stream_fail(openai): openai.api_key = "" with pytest.raises(Exception): - openai.OpenAI().completions.create( + openai.OpenAI().chat.completions.create( name=generation_name, model="gpt-3.5-turbo", prompt="1 + 1 = ", @@ -595,7 +595,7 @@ def test_openai_completion_with_langfuse_prompt(openai): prompt_client = langfuse.create_prompt( name=prompt_name, prompt="test prompt", labels=["production"] ) - openai.OpenAI().completions.create( + openai.OpenAI().chat.completions.create( name=generation_name, model="gpt-4.1", prompt="1 + 1 = ", From 16c282080e274a405daa31bd281cca949edf6720 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 30 Sep 2026 09:50:07 +0000 Subject: [PATCH 7/7] test(live-provider): skip legacy completion tests after gpt-3.5-turbo-instruct retirement Co-authored-by: Hassieb Pakzad --- tests/live_provider/test_langchain.py | 10 ++++-- .../test_langchain_integration.py | 18 +++++------ tests/live_provider/test_openai.py | 32 ++++++++++++------- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/tests/live_provider/test_langchain.py b/tests/live_provider/test_langchain.py index 1891d6db3..d04ddb81c 100644 --- a/tests/live_provider/test_langchain.py +++ b/tests/live_provider/test_langchain.py @@ -180,7 +180,7 @@ def test_callback_simple_openai(): # Initialize components handler = CallbackHandler() - llm = OpenAI() + llm = ChatOpenAI(model="gpt-4o-mini") text = ( "What would be a good company name for a company that makes colorful socks?" ) @@ -222,7 +222,7 @@ def test_callback_multiple_invocations_on_different_traces(): test_name_2 = f"Test Multiple Invocations 2 {create_uuid()}" # Setup components - llm = OpenAI() + llm = ChatOpenAI(model="gpt-4o-mini") text = ( "What would be a good company name for a company that makes colorful socks?" ) @@ -257,6 +257,10 @@ def test_callback_multiple_invocations_on_different_traces(): assert generation.output != "" +@pytest.mark.skip( + reason="OpenAI retired gpt-3.5-turbo-instruct, the legacy completions model " + "this test depends on" +) def test_openai_instruct_usage(): langfuse = Langfuse() @@ -280,7 +284,7 @@ def test_openai_instruct_usage(): """ ) | OpenAI( - model="gpt-4.1", + model="gpt-3.5-turbo-instruct", temperature=0, callbacks=[lf_handler], max_retries=3, diff --git a/tests/live_provider/test_langchain_integration.py b/tests/live_provider/test_langchain_integration.py index 055320de2..edb5455c4 100644 --- a/tests/live_provider/test_langchain_integration.py +++ b/tests/live_provider/test_langchain_integration.py @@ -77,7 +77,7 @@ def test_stream_chat_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_stream_completions_models(model_name): name = f"test_stream_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -181,7 +181,7 @@ def test_invoke_chat_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_invoke_in_completions_models(model_name): name = f"test_invoke_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -230,7 +230,7 @@ def test_invoke_in_completions_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_batch_in_completions_models(model_name): name = f"test_batch_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -386,7 +386,7 @@ async def test_astream_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_astream_completions_models(model_name): name = f"test_astream_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -494,7 +494,7 @@ async def test_ainvoke_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_ainvoke_in_completions_models(model_name): name = f"test_ainvoke_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -598,7 +598,7 @@ def test_chains_batch_in_chat_models(model_name): @pytest.mark.skip( reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_chains_batch_in_completions_models(model_name): name = f"test_chains_batch_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -704,7 +704,7 @@ async def test_chains_abatch_in_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_chains_abatch_in_completions_models(model_name): name = f"test_chains_abatch_in_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -809,7 +809,7 @@ async def test_chains_ainvoke_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_chains_ainvoke_completions_models(model_name): name = f"test_chains_ainvoke_completions_models-{create_uuid()}" tags = ["Hello", "world"] @@ -927,7 +927,7 @@ async def test_chains_astream_chat_models(model_name): reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" ) @pytest.mark.asyncio -@pytest.mark.parametrize("model_name", ["gpt-4.1"]) +@pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_chains_astream_completions_models(model_name): name = f"test_chains_astream_completions_models-{create_uuid()}" tags = ["Hello", "world"] diff --git a/tests/live_provider/test_openai.py b/tests/live_provider/test_openai.py index 96bad1423..43a07be23 100644 --- a/tests/live_provider/test_openai.py +++ b/tests/live_provider/test_openai.py @@ -10,6 +10,11 @@ langfuse: Langfuse | None = None +requires_legacy_completion_model = pytest.mark.skip( + reason="OpenAI retired gpt-3.5-turbo-instruct, the legacy completions model " + "this test depends on" +) + @pytest.fixture(scope="module") def openai(): @@ -405,11 +410,12 @@ def test_openai_chat_completion_with_seed(openai): assert len(completion.choices) != 0 +@requires_legacy_completion_model def test_openai_completion(openai): generation_name = create_uuid() - completion = openai.OpenAI().chat.completions.create( + completion = openai.OpenAI().completions.create( name=generation_name, - model="gpt-4.1", + model="gpt-3.5-turbo-instruct", prompt="1 + 1 = ", temperature=0, metadata={"someKey": "someResponse"}, @@ -428,7 +434,7 @@ def test_openai_completion(openai): assert completion.choices[0].text == generation.data[0].output assert generation.data[0].input == "1 + 1 = " assert generation.data[0].type == "GENERATION" - assert "gpt-4.1" in generation.data[0].model + assert "gpt-3.5-turbo-instruct" in generation.data[0].model assert generation.data[0].start_time is not None assert generation.data[0].end_time is not None assert generation.data[0].start_time < generation.data[0].end_time @@ -445,11 +451,12 @@ def test_openai_completion(openai): assert generation.data[0].output == "2\n\n1 + 2 = 3\n\n2 + 3 = " +@requires_legacy_completion_model def test_openai_completion_stream(openai): generation_name = create_uuid() - completion = openai.OpenAI().chat.completions.create( + completion = openai.OpenAI().completions.create( name=generation_name, - model="gpt-4.1", + model="gpt-3.5-turbo-instruct", prompt="1 + 1 = ", temperature=0, metadata={"someKey": "someResponse"}, @@ -475,7 +482,7 @@ def test_openai_completion_stream(openai): assert generation.data[0].input == "1 + 1 = " assert generation.data[0].type == "GENERATION" - assert "gpt-4.1" in generation.data[0].model + assert "gpt-3.5-turbo-instruct" in generation.data[0].model assert generation.data[0].start_time is not None assert generation.data[0].end_time is not None assert generation.data[0].start_time < generation.data[0].end_time @@ -504,7 +511,7 @@ def test_openai_completion_fail(openai): openai.api_key = "" with pytest.raises(Exception): - openai.OpenAI().chat.completions.create( + openai.OpenAI().completions.create( name=generation_name, model="fake", prompt="1 + 1 = ", @@ -546,7 +553,7 @@ def test_openai_completion_stream_fail(openai): openai.api_key = "" with pytest.raises(Exception): - openai.OpenAI().chat.completions.create( + openai.OpenAI().completions.create( name=generation_name, model="gpt-3.5-turbo", prompt="1 + 1 = ", @@ -588,6 +595,7 @@ def test_openai_completion_stream_fail(openai): openai.api_key = os.environ["OPENAI_API_KEY"] +@requires_legacy_completion_model def test_openai_completion_with_langfuse_prompt(openai): generation_name = create_uuid() langfuse = Langfuse() @@ -595,9 +603,9 @@ def test_openai_completion_with_langfuse_prompt(openai): prompt_client = langfuse.create_prompt( name=prompt_name, prompt="test prompt", labels=["production"] ) - openai.OpenAI().chat.completions.create( + openai.OpenAI().completions.create( name=generation_name, - model="gpt-4.1", + model="gpt-3.5-turbo-instruct", prompt="1 + 1 = ", temperature=0, metadata={"someKey": "someResponse"}, @@ -619,7 +627,7 @@ def test_fails_wrong_name(openai): with pytest.raises(TypeError, match="name must be a string"): openai.OpenAI().completions.create( name={"key": "generation_name"}, - model="gpt-4.1", + model="gpt-3.5-turbo-instruct", prompt="1 + 1 = ", temperature=0, ) @@ -629,7 +637,7 @@ def test_fails_wrong_trace_id(openai): with pytest.raises(TypeError, match="trace_id must be a string"): openai.OpenAI().completions.create( trace_id={"trace_id": "metadata"}, - model="gpt-4.1", + model="gpt-3.5-turbo-instruct", prompt="1 + 1 = ", temperature=0, )