Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
41617eb
chore!: require Python 3.11 or newer (#1927)
hassiebp Oct 7, 2026
5aae390
feat(openai)!: drop support for OpenAI SDK versions below 1.0 (#1928)
hassiebp Oct 7, 2026
10e4849
feat(otel)!: send x-langfuse-ingestion-version: 4 by default (#1930)
hassiebp Oct 7, 2026
049a6da
feat(client)!: remove blocked_instrumentation_scopes (#1931)
hassiebp Oct 7, 2026
8d7c066
feat(tracing)!: JSON-serialize propagated metadata values (#1932)
hassiebp Oct 7, 2026
3059c39
feat(client)!: remove get_dataset_run, get_dataset_runs and delete_da…
hassiebp Oct 7, 2026
661692b
feat(experiments)!: record experiment runs via OTEL only (#1934)
hassiebp Oct 7, 2026
944be04
feat(langchain)!: require LangChain v1 (langchain-core >= 1) (#1940)
hassiebp Oct 7, 2026
b4c284b
feat(batch-evaluation)!: evaluate observations via the v2 observation…
hassiebp Oct 7, 2026
fbe80b0
fix(tracing)!: require the gen_ai. prefix in the default span filter …
hassiebp Oct 7, 2026
6d8c760
feat(otel)!: compress span exports with gzip by default (#1954)
hassiebp Oct 7, 2026
fd7ae51
chore(api)!: drop references to API types removed from the v4 spec (#…
hassiebp Oct 9, 2026
8b9e83b
feat(api): update API spec from langfuse/langfuse 21b9b33 (#1961)
langfuse-bot Oct 9, 2026
18413db
feat(api): update API spec from langfuse/langfuse 5beee5d (#1962)
langfuse-bot Oct 9, 2026
f91a6aa
fix(tracing): keep metadata within the span attribute limit (#1957)
niklassemmler Oct 9, 2026
e99cc02
fix(tracing): mask dict metadata per key when the mask function fails…
niklassemmler Oct 9, 2026
edf44eb
test(api): pin how POST /scores responses parse into the union (#1964)
hassiebp Oct 9, 2026
988195e
feat(tracing)!: JSON-encode observation and trace metadata values (#1…
niklassemmler Oct 13, 2026
3b41467
fix(tracing): do not mask attributes the caller never set (#1973)
Neumanncheng Oct 13, 2026
4c845f8
test(tracing): expect JSON-encoded metadata in mask-unset test (#1985)
hassiebp Oct 13, 2026
82adbd7
chore: merge main into prepare-v5-release (#1986)
hassiebp Oct 13, 2026
dac9993
ci: test on Python 3.15 and declare it supported (#1988)
hassiebp Oct 13, 2026
db92337
feat(scores)!: send scores via POST /api/public/scores (#1987)
hassiebp Oct 13, 2026
43412fb
feat(api): update API spec from langfuse/langfuse 99da25d (#1990)
langfuse-bot Oct 13, 2026
abbc6cb
chore: merge main into prepare-v5-release (#1992)
hassiebp Oct 13, 2026
c92a1ca
chore: merge main into prepare-v5-release
cursoragent Oct 13, 2026
7f7b714
fix: address v5 release review findings (#1993)
hassiebp Oct 13, 2026
ae1e564
feat(api): update API spec from langfuse/langfuse 738b05d (#1995)
langfuse-bot Oct 13, 2026
63fcde7
refactor(experiments): drop sampling-aware gating of run-level scores…
hassiebp Oct 13, 2026
e68a2bf
test: read scores via api.scores.get_many in e2e helpers and docstrin…
hassiebp Oct 13, 2026
6599a97
feat(api): update API spec from langfuse/langfuse f0ea0c3 (#1999)
langfuse-bot Oct 13, 2026
d3a7d0e
feat(api): update API spec from langfuse/langfuse 7cb2eed (#2000)
langfuse-bot Oct 14, 2026
164c710
Merge remote-tracking branch 'origin/main' into prepare-v5-release
cursoragent Oct 14, 2026
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
7 changes: 0 additions & 7 deletions .github/langfuse-v4-dual-write.override.yml

This file was deleted.

18 changes: 15 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,23 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
- "3.15"

name: Unit tests on Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The pinned uv only knows Python 3.15 pre-releases; install the final release for uv to pick up.
- name: Set up Python 3.15
if: ${{ matrix.python-version == '3.15' }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.15"
- name: Install uv and set Python version
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
Expand All @@ -99,6 +105,14 @@ jobs:
python --version
uv run --frozen pytest -n auto --dist worksteal -s -v --log-cli-level=INFO tests/unit

# pydantic < 2.8 matches undiscriminated unions left to right, so the
# CreateScoresResponse member order matters there.
- name: Run score response parsing on pydantic 2.7
if: ${{ matrix.python-version == '3.11' }}
run: |
uv run --frozen --with "pydantic==2.7.4" python -c "import pydantic; assert pydantic.VERSION == '2.7.4', pydantic.VERSION"
uv run --frozen --with "pydantic==2.7.4" pytest -v tests/unit/test_scores_batch_response.py

e2e-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down Expand Up @@ -160,8 +174,6 @@ jobs:
LANGFUSE_SERVER_SHA="$(git ls-remote https://github.com/langfuse/langfuse.git HEAD | cut -f1)"
curl -fsSL "https://raw.githubusercontent.com/langfuse/langfuse/${LANGFUSE_SERVER_SHA}/docker-compose.yml" \
-o ./langfuse-server/docker-compose.yml
cp ./.github/langfuse-v4-dual-write.override.yml \
./langfuse-server/docker-compose.override.yml
echo "${LANGFUSE_SERVER_SHA}"

- name: Run langfuse server
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Minimum verification matrix:
The main CI workflow currently runs:
- linting on Python 3.13
- mypy on Python 3.13
- `tests/unit` on a Python 3.10-3.14 matrix
- `tests/unit` on a Python 3.11-3.15 matrix
- `tests/e2e` in 2 mechanical shards plus a serial subset inside each shard
- `tests/live_provider` as one always-on suite
- PR title validation for Conventional Commits
Expand Down
68 changes: 33 additions & 35 deletions langfuse/_client/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
)
from langfuse._utils.serializer import EventSerializer
from langfuse.api import MapValue
from langfuse.logger import langfuse_logger
from langfuse.model import PromptClient
from langfuse.types import SpanLevel

Expand All @@ -32,8 +33,6 @@ class LangfuseOtelSpanAttributes:
TRACE_TAGS = "langfuse.trace.tags"
TRACE_PUBLIC = "langfuse.trace.public"
TRACE_METADATA = "langfuse.trace.metadata"
TRACE_INPUT = "langfuse.trace.input"
TRACE_OUTPUT = "langfuse.trace.output"

# Langfuse-observation attributes
OBSERVATION_TYPE = "langfuse.observation.type"
Expand Down Expand Up @@ -68,20 +67,17 @@ class LangfuseOtelSpanAttributes:
EXPERIMENT_METADATA = "langfuse.experiment.metadata"
EXPERIMENT_DATASET_ID = "langfuse.experiment.dataset.id"
EXPERIMENT_ITEM_ID = "langfuse.experiment.item.id"
EXPERIMENT_ITEM_VERSION = "langfuse.experiment.item.version"
EXPERIMENT_ITEM_EXPECTED_OUTPUT = "langfuse.experiment.item.expected_output"
EXPERIMENT_ITEM_METADATA = "langfuse.experiment.item.metadata"
EXPERIMENT_ITEM_ROOT_OBSERVATION_ID = "langfuse.experiment.item.root_observation_id"


def create_trace_attributes(
*,
input: Optional[Any] = None,
output: Optional[Any] = None,
public: Optional[bool] = None,
) -> dict:
attributes = {
LangfuseOtelSpanAttributes.TRACE_INPUT: _serialize(input),
LangfuseOtelSpanAttributes.TRACE_OUTPUT: _serialize(output),
LangfuseOtelSpanAttributes.TRACE_PUBLIC: public,
}

Expand Down Expand Up @@ -165,30 +161,24 @@ def _serialize(obj: Any) -> Optional[str]:
return json.dumps(obj, cls=EventSerializer)


def _flatten_and_serialize_metadata_values(
metadata: Optional[Dict[str, Any]],
) -> Optional[Dict[str, str]]:
if metadata is None:
return None

flattened_metadata: Dict[str, str] = {}

def flatten_value(path: str, value: Any) -> None:
if isinstance(value, dict):
for nested_key, nested_value in value.items():
flatten_value(f"{path}.{nested_key}", nested_value)
def _serialize_metadata_value(value: Any) -> str:
"""JSON-encode one metadata value, strings included.

return
The server decodes metadata values from v5 SDKs, so encoding every value
keeps "123" and 123 distinct. EventSerializer turns ints outside the
JS-safe range and NaN/Infinity into JSON strings and never raises, so one
bad value cannot drop the other keys. Compact separators and raw non-ASCII
match JS JSON.stringify.
"""
# Plain strings skip EventSerializer, which returns them unchanged and then
# uses the same string encoder, so the output is identical but cheaper.
# str subclasses (e.g. str enums) still go through EventSerializer.
if type(value) is str:
return json.dumps(value, ensure_ascii=False)

serialized_value = _serialize(value)

if serialized_value is not None:
flattened_metadata[path] = serialized_value

for key, value in metadata.items():
flatten_value(str(key), value)

return flattened_metadata
return json.dumps(
value, cls=EventSerializer, separators=(",", ":"), ensure_ascii=False
)


def _flatten_and_serialize_metadata(
Expand All @@ -200,16 +190,24 @@ def _flatten_and_serialize_metadata(
else LangfuseOtelSpanAttributes.TRACE_METADATA
)

metadata_attributes: Dict[str, Union[str, int, None]] = {}
metadata_attributes: Dict[str, Optional[str]] = {}

if not isinstance(metadata, dict):
metadata_attributes[prefix] = _serialize(metadata)
# JSON-encode non-dict metadata too, strings included, matching the
# per-key values below. None stays None so it is not written.
metadata_attributes[prefix] = (
None if metadata is None else _serialize_metadata_value(metadata)
)
else:
for key, value in metadata.items():
metadata_attributes[f"{prefix}.{key}"] = (
value
if isinstance(value, str) or isinstance(value, int)
else _serialize(value)
)
# Skip None so an update does not overwrite an earlier value.
if value is None:
langfuse_logger.debug(
'Observation metadata key "%s" was not written because its value is None',
key,
)
continue

metadata_attributes[f"{prefix}.{key}"] = _serialize_metadata_value(value)

return metadata_attributes
Loading
Loading