feat(openai): capture service_tier in model_parameters#1751
Merged
Conversation
Capture the pricing-relevant service_tier attribute in the OpenAI integration: - Request side: extract the service_tier kwarg (guarding against NotGiven sentinels) and include it in model_parameters for chat completions and the Responses API. Embeddings are unchanged since they do not support service_tier. - Response side: read service_tier from the response in _get_langfuse_data_from_default_response and merge it into the request-side model_parameters on the generation update. The response value overrides the request value because OpenAI returns the tier that actually processed the request (e.g. when the request says "auto"), which is what pricing depends on. The merge preserves all request-side model parameters since span updates replace the whole model_parameters attribute. - Streaming: chat-completions streaming reads service_tier from the chunks; Responses API streaming reads it from the final response object. The request-side model_parameters are threaded through the stream wrappers so the finalized update can merge instead of clobber. Covers sync and async client paths, which share these helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@claude review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements LFE-10828: capture the pricing-relevant
service_tierattribute in the OpenAI monkeypatch integration (langfuse/openai.py).service_tieris extracted from kwargs in_get_langfuse_data_from_kwargs(guarding against openaiNotGivensentinels, like the other params) and included inmodel_parametersonly when actually set. Applies to chat completions and the Responses API; embeddings are untouched since they do not supportservice_tier._get_langfuse_data_from_default_responsenow also returns the response'sservice_tier, which is merged into the request-sidemodel_parameterson the generation update. The response value overrides the request value — OpenAI returns the tier that actually processed the request (e.g. when the request says"auto"), and the actually-used tier is the billing-relevant one.generation.update(model_parameters=...)replaces the whole serialized attribute (it does not merge), so the update mergesservice_tierinto the request-side dict (_merge_service_tier_into_model_parameters) instead of clobberingtemperature,top_p, etc. When the response carries noservice_tier, the update omitsmodel_parametersentirely, leaving the request-side value intact.service_tierfrom the chunks; Responses API streaming reads it from the final response object emitted by the stream. The request-sidemodel_parametersare threaded through both stream instrumentation paths (openai.Stream/AsyncStreamhooks and theLangfuseResponseGenerator{Sync,Async}fallbacks) so the finalized update can merge.Tests
New unit tests in
tests/unit/test_openai.py(written first, confirmed failing, then implemented):model_parametersNOT_GIVEN/ absent kwarg leaves the key absentservice_tieroverrides the request value while preserving other request-side model parameters (sync + async)service_tierfrom chunks (sync + async)model_parametersunaffectedservice_tierVerification
uv run --frozen pytest tests/unit/test_openai.py tests/unit/test_openai_prompt_extraction.py -q—48 passeduv run --frozen pytest -n auto --dist worksteal tests/unit -q—622 passed, 2 skipped, 18 errors(the 18 errors are pre-existingtests/unit/test_prompt.pysetup errors onmainin my local env — missing local Langfuse credentials — verified identical with the change stashed)uv run --frozen ruff check .—All checks passed!uv run --frozen ruff format --check .— only pre-existingtests/unit/test_experiment.pywould be reformatted (also fails onmain; not touched by this PR)uv run --frozen mypy langfuse --no-error-summary— exit 0Skipped
tests/e2eandtests/live_provider— out of scope per task; behavior is fully covered by exporter-local unit tests per the repo's testing policy.Notes
service_tierinside the generationmetadata(it is collected from the final response object); that behavior is unchanged — the value is now additionally merged intomodel_parameters._get_langfuse_data_from_default_response,_extract_streamed_openai_response,_extract_streamed_response_api_responsegained aservice_tierelement); the public stream wrapper classes only gained an optional keyword argument, so the change is backwards compatible.🤖 Generated with Claude Code
Greptile Summary
This PR adds
service_tiercapture to the OpenAI monkeypatch integration. On the request side,service_tieris extracted from kwargs (with aNotGivenguard) and stored inmodel_parameters; on the response side, the resolved tier is read from the non-streaming response, streaming chat chunks, and the final Response API event, then merged back via a new_merge_service_tier_into_model_parametershelper so the authoritative response value wins without clobbering other request-side parameters.model_parametersthrough to_finalize_stream_response, and the helper correctly returnsNone(suppressing the update) when the response carries noservice_tier._get_langfuse_data_from_default_response,_extract_streamed_openai_response, and_extract_streamed_response_api_responseare updated consistently across every call site.from openai._types import NOT_GIVENimport is placed inside a test function rather than at the top of the module; this should be moved to comply with the repo's import convention.Confidence Score: 4/5
Safe to merge; the only issue is a function-scoped import in a test that should be at module level.
The core implementation is solid: all three response paths (non-streaming, streaming chat, streaming Responses API) and their async counterparts correctly thread model_parameters through and merge the response-authoritative service_tier without clobbering unrelated parameters. The merge helper's early-return-None design cleanly avoids spurious updates when the response carries no service_tier. The one finding is an import inside a test function body instead of at the module top.
No files require special attention beyond the in-function import in tests/unit/test_openai.py.
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Caller participant wrap as _wrap / _wrap_async participant kwargs as _get_langfuse_data_from_kwargs participant resp as _get_langfuse_data_from_default_response participant merge as _merge_service_tier_into_model_parameters participant gen as LangfuseGeneration Caller->>wrap: "openai_client.chat.completions.create(service_tier=auto, ...)" wrap->>kwargs: extract request-side model_parameters Note over kwargs: parsed_service_tier captured if not NotGiven kwargs-->>wrap: langfuse_data (model_parameters incl. service_tier) wrap->>gen: "generation.create(model_parameters=...)" alt Non-streaming response wrap->>resp: _get_langfuse_data_from_default_response(resource, response) resp-->>wrap: (model, completion, usage, service_tier) wrap->>merge: _merge_service_tier_into_model_parameters(model_parameters, service_tier) merge-->>wrap: merged dict wrap->>gen: "generation.update(model_parameters=merged)" else Streaming response (chat) wrap->>wrap: "_instrument_openai_stream(model_parameters=...)" Note over wrap: _extract_streamed_openai_response reads service_tier from chunks wrap->>merge: _merge_service_tier_into_model_parameters(model_parameters, service_tier) merge-->>wrap: merged dict wrap->>gen: generation.update via _finalize_stream_response else Responses API streaming wrap->>wrap: _finalize_stream_response → _extract_streamed_response_api_response Note over wrap: service_tier read from response.completed chunk wrap->>merge: _merge_service_tier_into_model_parameters(model_parameters, service_tier) merge-->>wrap: merged dict wrap->>gen: generation.update end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Caller participant wrap as _wrap / _wrap_async participant kwargs as _get_langfuse_data_from_kwargs participant resp as _get_langfuse_data_from_default_response participant merge as _merge_service_tier_into_model_parameters participant gen as LangfuseGeneration Caller->>wrap: "openai_client.chat.completions.create(service_tier=auto, ...)" wrap->>kwargs: extract request-side model_parameters Note over kwargs: parsed_service_tier captured if not NotGiven kwargs-->>wrap: langfuse_data (model_parameters incl. service_tier) wrap->>gen: "generation.create(model_parameters=...)" alt Non-streaming response wrap->>resp: _get_langfuse_data_from_default_response(resource, response) resp-->>wrap: (model, completion, usage, service_tier) wrap->>merge: _merge_service_tier_into_model_parameters(model_parameters, service_tier) merge-->>wrap: merged dict wrap->>gen: "generation.update(model_parameters=merged)" else Streaming response (chat) wrap->>wrap: "_instrument_openai_stream(model_parameters=...)" Note over wrap: _extract_streamed_openai_response reads service_tier from chunks wrap->>merge: _merge_service_tier_into_model_parameters(model_parameters, service_tier) merge-->>wrap: merged dict wrap->>gen: generation.update via _finalize_stream_response else Responses API streaming wrap->>wrap: _finalize_stream_response → _extract_streamed_response_api_response Note over wrap: service_tier read from response.completed chunk wrap->>merge: _merge_service_tier_into_model_parameters(model_parameters, service_tier) merge-->>wrap: merged dict wrap->>gen: generation.update endPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(openai): capture service_tier in mo..." | Re-trigger Greptile
Context used:
Learned From
langfuse/langfuse-python#1387