diff --git a/AGENTS.md b/AGENTS.md index 932189e..305613a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -193,6 +193,8 @@ LaunchDarkly metadata attached to a flag variation. | `variationKey` | `str?` | Identifier for the specific variation. | | `version` | `int?` | Variation version number. | | `mode` | `"agent" \| "completion" \| "judge"` | Execution mode, used alongside `provider.name` to select a handler. | +| `modelKey` | `str?` | Stable key of the pinned model config, from `_ldMeta.modelKey`. Absent when the variation has no linked model config. Copied onto `TrackData`. | +| `modelVersion` | `int?` | Pinned model config version, from `_ldMeta.modelVersion`. Copied onto `TrackData`. | #### `ProviderResponse` @@ -240,6 +242,8 @@ Payload attached to every LaunchDarkly tracking event. | `version` | `int` | Variation version number. | | `modelName` | `str` | Model name from the config. | | `providerName` | `str` | Provider name from the config. | +| `modelKey` | `str?` | Stable key of the pinned model config, read from `_ldMeta.modelKey`. Omitted when the variation has no pinned model config. | +| `modelVersion` | `int?` | Pinned model config version, read from `_ldMeta.modelVersion`. Omitted when absent. | | `graphKey` | `str?` | Present when the event was produced inside an agent graph. | | `toolKey` | `str?` | Present when the event is for a tool call. | | `judgeConfigKey` | `str?` | Present when the event is from a judge execution. | diff --git a/packages/client/src/launchdarkly_ai_server/__init__.py b/packages/client/src/launchdarkly_ai_server/__init__.py index a2597e0..e05df90 100644 --- a/packages/client/src/launchdarkly_ai_server/__init__.py +++ b/packages/client/src/launchdarkly_ai_server/__init__.py @@ -109,8 +109,10 @@ end_unfinished_spans, lang_chain_span_usage, make_track_data, + model_stamps_from_meta, normalize_mode, number_or_zero, + omit_model_stamps, parse_json_with_possible_fences, parse_template, parse_usage, @@ -195,6 +197,8 @@ # utils "create_handler", "make_track_data", + "model_stamps_from_meta", + "omit_model_stamps", "normalize_mode", "parse_json_with_possible_fences", "parse_template", diff --git a/packages/client/src/launchdarkly_ai_server/graph.py b/packages/client/src/launchdarkly_ai_server/graph.py index e9088e7..4546c3b 100644 --- a/packages/client/src/launchdarkly_ai_server/graph.py +++ b/packages/client/src/launchdarkly_ai_server/graph.py @@ -24,7 +24,7 @@ UsageDict, VariationMeta, ) -from .utils import select_handler, to_ld_context +from .utils import model_stamps_from_meta, select_handler, to_ld_context logger = logging.getLogger(__name__) @@ -108,6 +108,7 @@ async def _build_graph( "version": meta.get("version", 1) if isinstance(meta, dict) else 1, "modelName": "", "providerName": "", + **model_stamps_from_meta(meta), "graphKey": key, } diff --git a/packages/client/src/launchdarkly_ai_server/judges.py b/packages/client/src/launchdarkly_ai_server/judges.py index 5878d6b..126cc70 100644 --- a/packages/client/src/launchdarkly_ai_server/judges.py +++ b/packages/client/src/launchdarkly_ai_server/judges.py @@ -26,6 +26,7 @@ ) from .utils import ( normalize_mode, + omit_model_stamps, to_ld_context, to_usage_dict, ) @@ -413,8 +414,11 @@ def _matches(h: ProviderHandler) -> bool: usage = to_usage_dict(raw_usage) + # A judge without a pinned model config must not inherit the parent's + # modelKey / modelVersion; every other parent-only key (graphKey, ...) + # is still carried over. merged_track_data: TrackData = { - **task.parent_track_data, + **omit_model_stamps(task.parent_track_data), **result["track_data"], "judgeConfigKey": task.config_key, } diff --git a/packages/client/src/launchdarkly_ai_server/lifecycle.py b/packages/client/src/launchdarkly_ai_server/lifecycle.py index 1a37b25..18e311c 100644 --- a/packages/client/src/launchdarkly_ai_server/lifecycle.py +++ b/packages/client/src/launchdarkly_ai_server/lifecycle.py @@ -8,6 +8,7 @@ from .sdk_info import flush_ai_sdk_info, reset_ai_sdk_info from .types import InitClientOptions +from .utils import model_stamps_from_meta logger = logging.getLogger(__name__) @@ -353,6 +354,9 @@ async def extract_variation( "variationKey": ld_meta.get("variationKey", ""), "version": ld_meta.get("version", 1), "mode": ld_meta.get("mode"), + # Pinned model-config identity; keys are omitted when absent so that + # tracking payloads never carry ``None`` values. + **model_stamps_from_meta(ld_meta), } # Strip _ldMeta for config parsing diff --git a/packages/client/src/launchdarkly_ai_server/tracking.py b/packages/client/src/launchdarkly_ai_server/tracking.py index 639ff68..9beae0b 100644 --- a/packages/client/src/launchdarkly_ai_server/tracking.py +++ b/packages/client/src/launchdarkly_ai_server/tracking.py @@ -17,7 +17,7 @@ TrackData, VariationMeta, ) -from .utils import parse_usage, to_ld_context +from .utils import model_stamps_from_meta, parse_usage, to_ld_context def _try_get_environment_id() -> str | None: @@ -129,6 +129,7 @@ async def execute_and_track( "providerName": config.get("provider", {}).get("name", "") if isinstance(config, dict) else "", + **model_stamps_from_meta(meta), } if graph_key: track_data["graphKey"] = graph_key @@ -210,6 +211,7 @@ async def execute_and_stream( "providerName": config.get("provider", {}).get("name", "") if isinstance(config, dict) else "", + **model_stamps_from_meta(meta), } if graph_key: track_data["graphKey"] = graph_key diff --git a/packages/client/src/launchdarkly_ai_server/types.py b/packages/client/src/launchdarkly_ai_server/types.py index f350536..dcbc570 100644 --- a/packages/client/src/launchdarkly_ai_server/types.py +++ b/packages/client/src/launchdarkly_ai_server/types.py @@ -97,7 +97,9 @@ class Message: VariationMeta = dict[str, Any] """ -Variation metadata: ``enabled``, ``variation_key``, ``version``, ``mode``. +Variation metadata: ``enabled``, ``variationKey``, ``version``, ``mode``, and +the pinned model-config identity ``modelKey`` / ``modelVersion`` (delivered in +``_ldMeta``; absent when no model config is pinned). """ # --------------------------------------------------------------------------- diff --git a/packages/client/src/launchdarkly_ai_server/utils.py b/packages/client/src/launchdarkly_ai_server/utils.py index 913ad40..2958de6 100644 --- a/packages/client/src/launchdarkly_ai_server/utils.py +++ b/packages/client/src/launchdarkly_ai_server/utils.py @@ -2,6 +2,7 @@ import json import re +from collections.abc import Mapping from dataclasses import dataclass, field from typing import Any, Literal @@ -538,6 +539,59 @@ def select_handler( raise ValueError(f"Handler for provider {provider} not found") +def model_stamps_from_meta(meta: Any) -> dict[str, Any]: + """ + Copies the pinned model-config identity (``modelKey``, ``modelVersion``) + from a variation's ``_ldMeta`` into a dict that can be merged into + ``TrackData``. Keys are omitted (never set to ``None``) when absent; an + empty ``modelKey`` is treated as absent and ``modelVersion`` is coerced to + ``int``. Gonfalon's cost attribution reads these two fields from every + ``$ld:ai:*`` event payload. + """ + if not isinstance(meta, dict): + return {} + stamps: dict[str, Any] = {} + model_key = meta.get("modelKey") + if isinstance(model_key, str) and model_key: + stamps["modelKey"] = model_key + model_version = _coerce_model_version(meta.get("modelVersion")) + if model_version is not None: + stamps["modelVersion"] = model_version + return stamps + + +def _coerce_model_version(value: Any) -> int | None: + """ + Coerces an ``_ldMeta.modelVersion`` value to ``int``. Accepts ``int`` + (but not ``bool``), integral ``float`` and integer-looking ``str``; + returns ``None`` for anything else. ``_ldMeta`` is an untyped flag payload, + so a malformed value must be dropped rather than abort the invocation. + """ + if isinstance(value, bool): + return None + if isinstance(value, int): + return value + if isinstance(value, float): + return int(value) if value.is_integer() else None + if isinstance(value, str): + try: + return int(value.strip()) + except ValueError: + return None + return None + + +def omit_model_stamps(track_data: Mapping[str, Any]) -> dict[str, Any]: + """ + Returns a copy of ``track_data`` without ``modelKey`` / ``modelVersion``. + Used when overlaying a judge's ``track_data`` on its parent's so a judge + without a pinned model config does not inherit the parent's identity. + """ + return { + k: v for k, v in track_data.items() if k not in ("modelKey", "modelVersion") + } + + def make_track_data(node: GraphNode, graph_key: str, run_id: str) -> dict[str, Any]: """ Builds the standard tracking payload for a graph node event. @@ -552,6 +606,7 @@ def make_track_data(node: GraphNode, graph_key: str, run_id: str) -> dict[str, A "version": meta.get("version", 1), "modelName": config.get("model", {}).get("name", ""), "providerName": config.get("provider", {}).get("name", ""), + **model_stamps_from_meta(meta), "graphKey": graph_key, } diff --git a/packages/client/tests/test_client.py b/packages/client/tests/test_client.py index eebd866..d735730 100644 --- a/packages/client/tests/test_client.py +++ b/packages/client/tests/test_client.py @@ -112,6 +112,74 @@ async def test_returns_provider_response(self, mock_ld_client: MagicMock) -> Non result = await m.invoke("q", CONTEXT) assert result.response == "answer" + async def test_track_data_copies_model_key_and_version_from_ld_meta( + self, mock_ld_client: MagicMock + ) -> None: + raw = await mock_ld_client.variation("flag", CONTEXT, None) + raw = { + **raw, + "_ldMeta": {**raw["_ldMeta"], "modelKey": "my-model", "modelVersion": 3}, + } + mock_ld_client.variation = AsyncMock(return_value=raw) + m = config(key="flag", handler=_make_handler()) + result = await m.invoke("q", CONTEXT) + assert result.track_data is not None + assert result.track_data["modelKey"] == "my-model" + assert result.track_data["modelVersion"] == 3 + assert isinstance(result.track_data["modelVersion"], int) + assert mock_ld_client.track.call_args_list + for call in mock_ld_client.track.call_args_list: + payload = call[0][2] + assert payload["modelKey"] == "my-model" + assert payload["modelVersion"] == 3 + + async def test_track_data_omits_model_key_and_version_when_absent( + self, mock_ld_client: MagicMock + ) -> None: + m = config(key="flag", handler=_make_handler()) + result = await m.invoke("q", CONTEXT) + assert result.track_data is not None + assert "modelKey" not in result.track_data + assert "modelVersion" not in result.track_data + for call in mock_ld_client.track.call_args_list: + payload = call[0][2] + assert "modelKey" not in payload + assert "modelVersion" not in payload + + async def test_track_data_treats_empty_model_key_as_absent( + self, mock_ld_client: MagicMock + ) -> None: + raw = await mock_ld_client.variation("flag", CONTEXT, None) + raw = { + **raw, + "_ldMeta": {**raw["_ldMeta"], "modelKey": "", "modelVersion": "2"}, + } + mock_ld_client.variation = AsyncMock(return_value=raw) + m = config(key="flag", handler=_make_handler()) + result = await m.invoke("q", CONTEXT) + assert result.track_data is not None + assert "modelKey" not in result.track_data + assert result.track_data["modelVersion"] == 2 + assert isinstance(result.track_data["modelVersion"], int) + + async def test_stream_track_data_copies_model_key_and_version( + self, mock_ld_client: MagicMock + ) -> None: + raw = await mock_ld_client.variation("flag", CONTEXT, None) + raw = { + **raw, + "_ldMeta": {**raw["_ldMeta"], "modelKey": "my-model", "modelVersion": 3}, + } + mock_ld_client.variation = AsyncMock(return_value=raw) + m = config(key="flag", handler=_make_handler(stream_chunks=["a", "b"])) + async for _ in m.stream("q", CONTEXT): + pass + assert mock_ld_client.track.call_args_list + for call in mock_ld_client.track.call_args_list: + payload = call[0][2] + assert payload["modelKey"] == "my-model" + assert payload["modelVersion"] == 3 + async def test_generation_success_on_success( self, mock_ld_client: MagicMock ) -> None: diff --git a/packages/client/tests/test_graph.py b/packages/client/tests/test_graph.py index 69a752f..3edffc1 100644 --- a/packages/client/tests/test_graph.py +++ b/packages/client/tests/test_graph.py @@ -192,6 +192,55 @@ async def test_traverses_root_leaf_returns_aggregated_result( result = await g.invoke("hi", CONTEXT) assert result.response is not None + async def test_graph_events_copy_model_key_and_version_from_ld_meta( + self, mock_ld_client: MagicMock + ) -> None: + graph_var = { + "_ldMeta": { + "enabled": True, + "variationKey": "gv1", + "version": 2, + "modelKey": "graph-model", + "modelVersion": 5, + }, + "root": "root-node", + "edges": {"root-node": [{"key": "leaf-node"}]}, + } + original = mock_ld_client.variation + + async def side_effect(key: str, ctx: dict, default: Any) -> Any: + if key == "graph-key": + return graph_var + return await original(key, ctx, default) + + mock_ld_client.variation = AsyncMock(side_effect=side_effect) + g = graph("graph-key", handlers=[_make_handler()]) + await g.invoke("hi", CONTEXT) + graph_calls = [ + c + for c in mock_ld_client.track.call_args_list + if str(c[0][0]).startswith("$ld:ai:graph:") + ] + assert graph_calls + for c in graph_calls: + assert c[0][2]["modelKey"] == "graph-model" + assert c[0][2]["modelVersion"] == 5 + + async def test_graph_events_omit_model_key_and_version_when_absent( + self, mock_ld_client: MagicMock + ) -> None: + g = graph("graph-key", handlers=[_make_handler()]) + await g.invoke("hi", CONTEXT) + graph_calls = [ + c + for c in mock_ld_client.track.call_args_list + if str(c[0][0]).startswith("$ld:ai:graph:") + ] + assert graph_calls + for c in graph_calls: + assert "modelKey" not in c[0][2] + assert "modelVersion" not in c[0][2] + async def test_graph_duration_total_tracked( self, mock_ld_client: MagicMock ) -> None: diff --git a/packages/client/tests/test_judges.py b/packages/client/tests/test_judges.py index 38b289c..5d56094 100644 --- a/packages/client/tests/test_judges.py +++ b/packages/client/tests/test_judges.py @@ -3,7 +3,7 @@ Reference: TESTING.md §3.14 """ -from typing import Any +from typing import Any, ClassVar from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -478,3 +478,112 @@ async def judge_fn( assert recorded == [] assert result.score is None assert result.response == "cannot tell" + + +class TestRunJudgeTrackData: + """§3.13 run_judge result track_data must not inherit the parent's model stamps.""" + + PARENT: ClassVar[dict[str, Any]] = { + "runId": "parent-run", + "configKey": "main-flag", + "variationKey": "v1", + "version": 1, + "modelName": "gpt-4o", + "providerName": "OpenAI", + "modelKey": "parent-model", + "modelVersion": 7, + "graphKey": "g1", + } + + async def _run( + self, monkeypatch: pytest.MonkeyPatch, judge_track_data: dict[str, Any] + ) -> Any: + from contextlib import asynccontextmanager + + import launchdarkly_ai_server.judges as judges_module + import launchdarkly_ai_server.tracking as tracking_module + from launchdarkly_ai_server import JudgeTask, run_judge + + @asynccontextmanager + async def fake_with_judge_evaluation(name: str) -> Any: + yield lambda score, explanation=None: None + + monkeypatch.setattr( + judges_module, "with_judge_evaluation", fake_with_judge_evaluation + ) + + async def fake_execute_and_track(**kwargs: Any) -> dict[str, Any]: + return { + "response": '{"score": 0.9, "reasoning": "good"}', + "usage": {"input_tokens": 1, "output_tokens": 1}, + "track_data": judge_track_data, + } + + monkeypatch.setattr( + tracking_module, "execute_and_track", fake_execute_and_track + ) + + async def judge_fn( + config, user_input, tool_handlers, variables, history=None + ) -> dict: # type: ignore[override] + raise AssertionError("execute_and_track is stubbed") + + handler = ProviderHandler( + fn=judge_fn, provides_for=("TestProvider", "messages") + ) # type: ignore[arg-type] + task = JudgeTask( + config_key="judge-key", + judge_config={ + "model": {"name": "claude"}, + "provider": {"name": "TestProvider"}, + "instructions": "judge", + }, + judge_meta={"enabled": True, "variationKey": "j1", "version": 1}, + actual_output="response", + user_context=CONTEXT, + judge_provider="TestProvider", + judge_mode="messages", + collapse_messages=False, + parent_track_data=dict(self.PARENT), + ) + return await run_judge(task, [handler]) + + async def test_does_not_inherit_parent_model_stamps( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + result = await self._run( + monkeypatch, + { + "runId": "judge-run", + "configKey": "judge-key", + "variationKey": "j1", + "version": 1, + "modelName": "claude", + "providerName": "TestProvider", + }, + ) + assert result is not None + assert "modelKey" not in result.track_data + assert "modelVersion" not in result.track_data + assert result.track_data["judgeConfigKey"] == "judge-key" + assert result.track_data["graphKey"] == "g1" + + async def test_keeps_judge_own_model_stamps( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + result = await self._run( + monkeypatch, + { + "runId": "judge-run", + "configKey": "judge-key", + "variationKey": "j1", + "version": 1, + "modelName": "claude", + "providerName": "TestProvider", + "modelKey": "judge-model", + "modelVersion": 2, + }, + ) + assert result is not None + assert result.track_data["modelKey"] == "judge-model" + assert result.track_data["modelVersion"] == 2 diff --git a/packages/client/tests/test_lifecycle.py b/packages/client/tests/test_lifecycle.py index 9f7f211..60bf35f 100644 --- a/packages/client/tests/test_lifecycle.py +++ b/packages/client/tests/test_lifecycle.py @@ -518,6 +518,38 @@ async def test_returns_enabled_true_and_config_when_variation_is_enabled( assert result["config"]["model"]["name"] == "claude-3-5" # type: ignore[index] assert result["meta"] is not None + async def test_preserves_model_key_and_version_on_meta(self) -> None: + stub = _make_stub_client() + stub.variation = AsyncMock( + return_value={ + "_ldMeta": { + "enabled": True, + "variationKey": "v1", + "version": 1, + "modelKey": "my-model", + "modelVersion": 3, + }, + "model": {"name": "claude-3-5"}, + "provider": {"name": "Anthropic"}, + "instructions": "You are helpful.", + } + ) + with patch.object(lifecycle_module, "_setup_telemetry", return_value=None): + await init_client(client=stub) + ctx = {"kind": "user", "key": "user-1"} + with patch( + "launchdarkly_ai_server.utils.to_ld_context", + side_effect=lambda _c, ctx: ctx, + ): + result = await inspect_config("my-flag", ctx) + extracted = await lifecycle_module.extract_variation("my-flag", ctx) + + assert result["meta"] is not None + assert result["meta"]["modelKey"] == "my-model" # type: ignore[index] + assert result["meta"]["modelVersion"] == 3 # type: ignore[index] + assert extracted["meta"]["modelKey"] == "my-model" + assert extracted["meta"]["modelVersion"] == 3 + async def test_returns_enabled_false_and_null_config_when_disabled(self) -> None: stub = _make_stub_client() stub.variation = AsyncMock(return_value={"_ldMeta": {"enabled": False}}) diff --git a/packages/client/tests/test_utils.py b/packages/client/tests/test_utils.py index 42e9d4b..88f0529 100644 --- a/packages/client/tests/test_utils.py +++ b/packages/client/tests/test_utils.py @@ -4,11 +4,16 @@ Reference: TESTING.md s3.1-3.4, s3.15 """ +from typing import Any + import pytest from launchdarkly_ai_server import ( create_handler, + make_track_data, + model_stamps_from_meta, normalize_mode, + omit_model_stamps, parse_json_with_possible_fences, parse_template, parse_usage, @@ -262,3 +267,84 @@ async def fn( assert callable(h) # The original function is accessible via _fn attribute assert h._fn is fn + + +class TestMakeTrackData: + """§3.10 model stamps — shared node-trackData builder for native graph adapters.""" + + def _node(self, meta: dict) -> Any: + from launchdarkly_ai_server.types import GraphNode + + return GraphNode( + key="node-a", + config={"model": {"name": "gpt-4"}, "provider": {"name": "OpenAI"}}, + meta=meta, + ) + + def test_copies_model_key_and_version_from_meta(self) -> None: + td = make_track_data( + self._node( + { + "variationKey": "v1", + "version": 1, + "modelKey": "my-model", + "modelVersion": 3, + } + ), + "graph-key", + "run-1", + ) + assert td["modelKey"] == "my-model" + assert td["modelVersion"] == 3 + assert td["graphKey"] == "graph-key" + + def test_omits_model_key_and_version_when_absent(self) -> None: + td = make_track_data( + self._node({"variationKey": "v1", "version": 1}), "graph-key", "run-1" + ) + assert "modelKey" not in td + assert "modelVersion" not in td + + +class TestModelStampsFromMeta: + """§3.10 model stamps — malformed ``modelVersion`` is omitted, never raises.""" + + def test_copies_int_version_and_non_empty_key(self) -> None: + assert model_stamps_from_meta({"modelKey": "m", "modelVersion": 3}) == { + "modelKey": "m", + "modelVersion": 3, + } + + @pytest.mark.parametrize("value", ["3", 3.0]) + def test_coerces_integral_string_and_float(self, value: Any) -> None: + assert model_stamps_from_meta({"modelVersion": value}) == {"modelVersion": 3} + + @pytest.mark.parametrize( + "value", + ["abc", "1.5", 1.5, {}, [], None, True, False, float("nan"), "", " "], + ) + def test_omits_malformed_version_without_raising(self, value: Any) -> None: + stamps = model_stamps_from_meta({"modelVersion": value, "modelKey": "m"}) + assert "modelVersion" not in stamps + assert stamps["modelKey"] == "m" + + @pytest.mark.parametrize("value", [123, {}, ["a"], True, ""]) + def test_omits_non_string_or_empty_model_key(self, value: Any) -> None: + stamps = model_stamps_from_meta({"modelKey": value, "modelVersion": 1}) + assert "modelKey" not in stamps + assert stamps["modelVersion"] == 1 + + def test_non_dict_meta_returns_empty(self) -> None: + assert model_stamps_from_meta(None) == {} + assert model_stamps_from_meta("nope") == {} + + +class TestOmitModelStamps: + def test_removes_only_the_two_stamp_keys(self) -> None: + td = {"runId": "r", "graphKey": "g", "modelKey": "m", "modelVersion": 1} + assert omit_model_stamps(td) == {"runId": "r", "graphKey": "g"} + + def test_does_not_mutate_input(self) -> None: + td = {"runId": "r", "modelKey": "m"} + omit_model_stamps(td) + assert td == {"runId": "r", "modelKey": "m"} diff --git a/uv.lock b/uv.lock index c3e6d32..c1c9677 100644 --- a/uv.lock +++ b/uv.lock @@ -790,7 +790,7 @@ wheels = [ [[package]] name = "launchdarkly-ai-claude-agents" -version = "0.2.1" +version = "0.2.2" source = { editable = "packages/claude-agents" } dependencies = [ { name = "anthropic" }, @@ -809,7 +809,7 @@ requires-dist = [ [[package]] name = "launchdarkly-ai-claude-messages" -version = "0.2.1" +version = "0.2.2" source = { editable = "packages/claude-messages" } dependencies = [ { name = "anthropic" }, @@ -826,7 +826,7 @@ requires-dist = [ [[package]] name = "launchdarkly-ai-langchain-agents" -version = "0.2.1" +version = "0.2.2" source = { editable = "packages/langchain-agents" } dependencies = [ { name = "langchain-core" }, @@ -845,7 +845,7 @@ requires-dist = [ [[package]] name = "launchdarkly-ai-langchain-messages" -version = "0.2.1" +version = "0.2.2" source = { editable = "packages/langchain-messages" } dependencies = [ { name = "langchain-core" }, @@ -862,7 +862,7 @@ requires-dist = [ [[package]] name = "launchdarkly-ai-openai-agents" -version = "0.2.1" +version = "0.2.2" source = { editable = "packages/openai-agents" } dependencies = [ { name = "launchdarkly-ai-server" }, @@ -881,7 +881,7 @@ requires-dist = [ [[package]] name = "launchdarkly-ai-openai-messages" -version = "0.2.1" +version = "0.2.2" source = { editable = "packages/openai-messages" } dependencies = [ { name = "launchdarkly-ai-server" }, @@ -898,7 +898,7 @@ requires-dist = [ [[package]] name = "launchdarkly-ai-python" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/ai" } dependencies = [ { name = "launchdarkly-ai-server" }, @@ -918,7 +918,7 @@ provides-extras = ["otel"] [[package]] name = "launchdarkly-ai-server" -version = "0.2.1" +version = "0.2.2" source = { editable = "packages/client" } dependencies = [ { name = "opentelemetry-api" },