fix(otel): populate gen_ai.output.messages and gen_ai.system_instructions for Responses API - #26670
Conversation
[Infra] Promote internal staging to main
* feat(openai): day-0 support for GPT-5.5 and GPT-5.5 Pro
Add pricing + capability entries for the new GPT-5.5 family launched by
OpenAI on 2026-04-24:
- gpt-5.5 / gpt-5.5-2026-04-23 (chat): $5/$30/$0.50 per 1M
input/output/cached input
- gpt-5.5-pro / gpt-5.5-pro-2026-04-23 (responses-only): $60/$360/$6
per 1M input/output/cached input
Other fees (long-context >272k, flex, batches, priority, cache
discounts) follow the same ratios as GPT-5.4, with context window
retained at 1.05M input / 128K output.
No transformation / classifier code changes are required:
OpenAIGPT5Config.is_model_gpt_5_4_plus_model() already matches 5.5+ via
numeric version parsing, and model registration is driven from the
JSON. The existing responses-API bridge for tools + reasoning_effort
(litellm/main.py:970) already covers gpt-5.5-pro.
Tests:
- GPT5_MODELS regression list now covers gpt-5.5-pro and dated variants
- New test_generic_cost_per_token_gpt55_pro cost-calc test
- Updated test_generic_cost_per_token_gpt55 for long-context fields
* fix(openai): mirror reasoning_effort flags onto gpt-5.5 dated variants
gpt-5.5-2026-04-23 and gpt-5.5-pro-2026-04-23 were missing the
supports_none_reasoning_effort, supports_xhigh_reasoning_effort, and
supports_minimal_reasoning_effort flags that their non-dated
counterparts define. Reasoning-effort routing in OpenAIGPT5Config is
fully capability-driven from these JSON flags — since an absent flag
is treated as False for opt-in levels (xhigh), users pinning to a
dated snapshot would silently lose xhigh support and diverge from the
base alias on logprobs + flexible temperature handling.
Copy the flags onto both dated variants so every dated snapshot
inherits the base model's reasoning-effort capability profile.
Adds a parametrized regression test that asserts
supports_{none,minimal,xhigh}_reasoning_effort parity between each
dated variant and its non-dated counterpart, preventing future drift
when new snapshots are added.
…s) (BerriAI#26361) * feat(azure): add azure/gpt-5.5 + azure/gpt-5.5-pro entries (+ dated variants) Azure variants of OpenAI's GPT-5.5 family. Microsoft has not yet shipped GPT-5.5 on Azure OpenAI (latest GA on the Foundry models page is GPT-5.4 as of 2026-04-24), but adding the entries day-0 mirrors the established precedent for azure/gpt-5.4* (which were in the cost map before the Azure rollout) so cost tracking and capability flags work the moment customers deploy. Schema follows the existing azure/gpt-5.4* shape: - Same base/long-context pricing as openai/gpt-5.5*: $5/$30 chat, $60/$360 pro per 1M, with priority tier 2x base - Azure variants drop the flex/batches keys (Azure has no flex tier) but keep priority pricing, matching gpt-5.4* precedent - mode=chat for the thinking model, mode=responses for pro reasoning_effort capability flags mirror the OpenAI variants exactly since Azure proxies the same API contract: minimal rejection on both chat and pro, low/none rejection on pro. Once BerriAI#26456 (which sets supports_low_reasoning_effort + minimal=false on openai/gpt-5.5*) lands, OpenAI and Azure flag profiles align. Tests pin entry presence + pricing for all four Azure variants and verify the live-API-derived reasoning_effort flags. * test: register supports_low_reasoning_effort in cost-map JSON schema azure/gpt-5.5-pro and azure/gpt-5.5-pro-2026-04-23 added in this branch carry supports_low_reasoning_effort=false. The strict 'additionalProperties: false' schema in test_aaamodel_prices_and_context_window_json_is_valid rejected the new key. Register it alongside the other supports_*_reasoning_effort entries. Note: the runtime side of this flag (code that reads it) lands in BerriAI#26456. Until that PR merges the flag is inert for both Azure and OpenAI pro entries, but having the schema accept it lets cost-map tests pass on either merge order.
[Infra] Promote Internal Staging to main
…ions for Responses API Fixes BerriAI#25840 The OTel integration's set_attributes() method never populates gen_ai.output.messages, gen_ai.system_instructions, or gen_ai.response.finish_reasons for /v1/responses calls because ResponsesAPIResponse uses 'output' instead of 'choices' and the system prompt arrives as 'instructions' instead of 'system_instructions'. Changes: - Add elif branch for response_obj.get('output') to extract response text from Responses API output items (type='message'/output_text) and tool calls (type='function_call') - Coalesce system_instructions/instructions/system kwargs so the system prompt is captured for Responses API, Anthropic Messages API, and Vertex AI Gemini paths - Handle plain-string system prompts without unnecessary wrapping - Extract response_obj.get('status') as finish reason for Responses API - Add _transform_responses_api_output_to_otel() method
…uctions, and finish reasons Add 21 tests covering the new Responses API OTel attribute handling: TestOpenTelemetryResponsesAPI (13 tests): - gen_ai.output.messages from output items (text, function_call, mixed, multi-part) - gen_ai.response.finish_reasons from ResponsesAPIResponse.status - gen_ai.system_instructions from instructions/system/system_instructions kwargs - Precedence and absence edge cases - Regression test for existing choices-based responses TestTransformResponsesAPIOutput (8 tests): - Message with output_text, function_call items, unknown types - Edge cases: empty output, empty text, missing call_id, default role, non-dict items
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes missing OTel span attributes ( Confidence Score: 5/5Safe to merge — changes are additive elif branches and a new private method; existing choices path is untouched. No P0 or P1 findings. All four issues flagged in the prior review round (falsy system_instructions fallthrough, missing per-tool-call attributes, isinstance(dict) Pydantic guard, duplicate finish-reasons block) are resolved in the final file. New code paths are guarded by the existing outer try/except and covered by 21 new mock-based unit tests. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/integrations/opentelemetry.py | Adds elif branch for Responses API output/status, broadens system-prompt kwarg coalescing to include instructions/system, and introduces _transform_responses_api_output_to_otel; all four issues from prior review round addressed in the final file. |
| tests/test_litellm/integrations/test_opentelemetry.py | 21 new unit tests added covering output messages, finish reasons, system instructions, per-tool-call attributes, Pydantic duck-typing, and regression for existing choices path; all use MagicMock with no real network calls. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["set_attributes(span, kwargs, response_obj)"] --> B{response_obj is not None?}
B -- No --> Z[return]
B -- Yes --> C{response_obj.get choices ?}
C -- Yes --> D["_transform_choices_to_otel_semantic_conventions()"]
D --> E["set gen_ai.output.messages"]
E --> F["extract finish_reasons from choices[].finish_reason"]
F --> G["set gen_ai.response.finish_reasons"]
G --> H["for each choice: _tool_calls_kv_pair()"]
H --> I["set gen_ai.completion.N.function_call.* attributes"]
C -- No --> J{response_obj.get output ?}
J -- No --> Z
J -- Yes --> K["_transform_responses_api_output_to_otel(output_items)"]
K --> L["set gen_ai.output.messages"]
L --> M["for each function_call item: build tool_calls list"]
M --> N["_tool_calls_kv_pair(tool_calls)"]
N --> O["set gen_ai.completion.N.function_call.* attributes"]
O --> P["response_obj.get status"]
P --> Q["set gen_ai.response.finish_reasons = [status]"]
A --> R["coalesce system_instructions / instructions / system"]
R --> S{isinstance str?}
S -- Yes --> T["set gen_ai.system_instructions = raw string"]
S -- No --> U["_transform_messages_to_otel_semantic_conventions()"]
U --> V["set gen_ai.system_instructions = JSON"]
Reviews (5): Last reviewed commit: "fix: remove duplicate gen_ai.response.fi..." | Re-trigger Greptile
Build the tool_call part dict separately with an explicit type annotation so mypy can track the type, avoiding the 'Unsupported target for indexed assignment' error on tool_call["parts"][0]["id"].
…r-tool-call attrs - Replace isinstance(item, dict) with hasattr(item, 'get') so Pydantic model instances (ResponseOutputMessage, ResponseFunctionToolCall) are accepted alongside plain dicts (P1) - Use 'is not None' guards instead of or-chain for system_instructions coalescing to prevent falsy values (e.g. []) falling through to the wrong kwarg (P2) - Emit per-tool-call span attributes (gen_ai.completion.N.function_call.*) for Responses API function_call items, matching the choices branch parity with _tool_calls_kv_pair (P2) - Add 4 new tests: Pydantic-like objects, falsy fallthrough guard, per-tool-call attribute emission, multiple tool call indexing
Review Comments — All ResolvedAll four Greptile review findings have been addressed:
|
|
@aneeshsangvikar — could you add a screenshot or short video showing that this change works as expected? It really helps reviewers verify the fix quickly. Thanks! |
VerificationHere's the output of calling Before the fix: After the fix: All three are populated using the Responses API equivalents ( Reproduction script (click to expand)from unittest.mock import MagicMock
from litellm.integrations.opentelemetry import OpenTelemetry
otel = OpenTelemetry()
mock_span = MagicMock()
kwargs = {
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "What is 2+2?"}],
"instructions": "You are a helpful math tutor.",
"optional_params": {},
"litellm_params": {"custom_llm_provider": "openai"},
"standard_logging_object": {
"id": "resp_abc123",
"call_type": "responses",
"metadata": {},
},
}
response_obj = {
"id": "resp_abc123",
"model": "gpt-4o-mini-2024-07-18",
"status": "completed",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "2 + 2 = 4."}
],
}
],
"usage": {"prompt_tokens": 25, "completion_tokens": 10, "total_tokens": 35},
}
otel.set_attributes(span=mock_span, kwargs=kwargs, response_obj=response_obj)
for call in mock_span.set_attribute.call_args_list:
print(f" {call[0][0]}: {call[0][1]}") |
|
@krrish-berri-2 Thanks for reviewing! The verification is in this comment above — it shows the before/after span attributes for a In short, the three previously-missing attributes are now populated:
All 126 unit tests pass (101 existing + 25 new), and all 42 CI checks are green. |
…mation The openai SDK returns ResponseOutputMessage and ResponseOutputText as raw Pydantic v2 models that lack .get() (unlike LiteLLM's own wrapper objects). Add a _to_dict() helper that normalizes plain dicts, BaseLiteLLMOpenAIResponseObject (has .get()), and raw Pydantic models (has .model_dump()) into a consistent dict interface.
|
|
🤖 litellm-agent: Merged into staging branch Triage Summary Merge Confidence: 5/5 ✅ READY All checks green. Greptile 5/5, no blocking pattern findings, no CircleCI runs (OSS-typical). |

Summary
Fixes #25840
The OTel integration's
set_attributes()method never populatesgen_ai.output.messages,gen_ai.system_instructions, orgen_ai.response.finish_reasonsfor/v1/responses(Responses API) calls. Token counts, costs, and input messages all work correctly — only the output content, system prompt, and finish reasons are missing.Root Cause
gen_ai.output.messages: The code only checksresponse_obj.get("choices"), butResponsesAPIResponseusesoutput(a list of output items withtype="message"/type="output_text") instead ofchoices. Soresponse_obj.get("choices")returnsNoneand the entire block is skipped.gen_ai.system_instructions: The code only checkskwargs.get("system_instructions")(Vertex AI Gemini path), but the Responses API passes the system prompt askwargs["instructions"], and the Anthropic Messages API useskwargs["system"].gen_ai.response.finish_reasons: Derived fromchoices[].finish_reason, which doesn't exist for Responses API. The equivalent isresponse_obj.get("status")(e.g."completed").Changes
1. Output messages (
set_attributes())Added an
elif response_obj.get("output")branch after the existingchoicescheck. A new method_transform_responses_api_output_to_otel()converts the Responses API output structure to the same{"role": ..., "parts": [...]}format used by chat completions. Handles bothtype="message"items (text output) andtype="function_call"items (tool calls).2. System instructions (
set_attributes())Coalesced three kwarg names that carry the same semantic data:
system_instructionsinstructionssystemAlso handles plain strings (common for Responses API
instructions) without wrapping them in a message array.3. Finish reasons (
set_attributes())For Responses API, extracts
response_obj.get("status")(e.g."completed") as the finish reason, since there are nochoicesto pullfinish_reasonfrom.Testing
21 new unit tests added in
tests/test_litellm/integrations/test_opentelemetry.py:TestOpenTelemetryResponsesAPI(13 tests):gen_ai.output.messagesfrom output items (text, function_call, mixed, multi-part, empty text)gen_ai.response.finish_reasonsfromResponsesAPIResponse.statusgen_ai.system_instructionsfrominstructions/system/system_instructionskwargschoices-based responses still workTestTransformResponsesAPIOutput(8 tests):output_text,function_callitems, unknown typescall_id, default role, non-dict itemsAll 122 tests pass (101 existing + 21 new).
Impact
Any downstream OTel consumer (Datadog, Honeycomb, Fiddler, Phoenix, custom pipelines) that relies on
gen_ai.output.messageswill now receive response content for all/v1/responsescalls. Previously these appeared as empty output in observability dashboards even though the LLM call succeeded and tokens were counted correctly.The
gen_ai.system_instructionsfix also benefits the Anthropic Messages API pass-through endpoint where the system prompt was silently dropped.Existing
/chat/completionspaths are unaffected — the new code is entirely inelifbranches.