Skip to content

fix(otel): populate gen_ai.output.messages and gen_ai.system_instructions for Responses API - #26670

Closed
aneeshsangvikar wants to merge 12 commits into
BerriAI:litellm_oss_staging_04_27_2026from
aneeshsangvikar:fix/otel-responses-api-clean
Closed

fix(otel): populate gen_ai.output.messages and gen_ai.system_instructions for Responses API#26670
aneeshsangvikar wants to merge 12 commits into
BerriAI:litellm_oss_staging_04_27_2026from
aneeshsangvikar:fix/otel-responses-api-clean

Conversation

@aneeshsangvikar

Copy link
Copy Markdown
Contributor

Summary

Fixes #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 (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 checks response_obj.get("choices"), but ResponsesAPIResponse uses output (a list of output items with type="message" / type="output_text") instead of choices. So response_obj.get("choices") returns None and the entire block is skipped.
  • gen_ai.system_instructions: The code only checks kwargs.get("system_instructions") (Vertex AI Gemini path), but the Responses API passes the system prompt as kwargs["instructions"], and the Anthropic Messages API uses kwargs["system"].
  • gen_ai.response.finish_reasons: Derived from choices[].finish_reason, which doesn't exist for Responses API. The equivalent is response_obj.get("status") (e.g. "completed").

Changes

1. Output messages (set_attributes())

Added an elif response_obj.get("output") branch after the existing choices check. 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 both type="message" items (text output) and type="function_call" items (tool calls).

2. System instructions (set_attributes())

Coalesced three kwarg names that carry the same semantic data:

Call path Kwarg name Previously captured?
Vertex AI Gemini chat-completion system_instructions Yes
OpenAI Responses API instructions No
Anthropic Messages API system No

Also 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 no choices to pull finish_reason from.

Testing

21 new unit tests added in tests/test_litellm/integrations/test_opentelemetry.py:

TestOpenTelemetryResponsesAPI (13 tests):

  • gen_ai.output.messages from output items (text, function_call, mixed, multi-part, empty text)
  • 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 confirming existing choices-based responses still work

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

All 122 tests pass (101 existing + 21 new).

Impact

Any downstream OTel consumer (Datadog, Honeycomb, Fiddler, Phoenix, custom pipelines) that relies on gen_ai.output.messages will now receive response content for all /v1/responses calls. Previously these appeared as empty output in observability dashboards even though the LLM call succeeded and tokens were counted correctly.

The gen_ai.system_instructions fix also benefits the Anthropic Messages API pass-through endpoint where the system prompt was silently dropped.

Existing /chat/completions paths are unaffected — the new code is entirely in elif branches.

yuneng-berri and others added 6 commits April 23, 2026 17:55
* 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.
…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
@CLAassistant

CLAassistant commented Apr 28, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ yuneng-berri
✅ mateo-berri
❌ Aneesh-Fiddler
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed-hq

codspeed-hq Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing aneeshsangvikar:fix/otel-responses-api-clean (9928618) with main (3d2b8fe)

Open in CodSpeed

@codecov

codecov Bot commented Apr 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49123% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/integrations/opentelemetry.py 96.49% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes missing OTel span attributes (gen_ai.output.messages, gen_ai.system_instructions, gen_ai.response.finish_reasons) for /v1/responses (Responses API) calls by adding an elif response_obj.get("output") branch alongside the existing choices path, and broadening the system-prompt coalescing to include instructions (Responses API) and system (Anthropic) kwargs. All four issues flagged in the previous review round (falsy system_instructions fallthrough, missing per-tool-call attributes, isinstance(dict) guard blocking Pydantic objects, duplicate finish-reasons block) appear to have been addressed in the final code.

Confidence Score: 5/5

Safe 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.

Important Files Changed

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"]
Loading

Reviews (5): Last reviewed commit: "fix: remove duplicate gen_ai.response.fi..." | Re-trigger Greptile

Comment thread litellm/integrations/opentelemetry.py
Comment thread litellm/integrations/opentelemetry.py
Comment thread litellm/integrations/opentelemetry.py Outdated
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
Comment thread litellm/integrations/opentelemetry.py Outdated
@aneeshsangvikar
aneeshsangvikar changed the base branch from main to litellm_oss_staging_04_27_2026 April 29, 2026 04:41
@aneeshsangvikar

Copy link
Copy Markdown
Contributor Author

Review Comments — All Resolved

All four Greptile review findings have been addressed:

Finding Severity Resolution Commit
isinstance(dict) drops Pydantic objects P1 Replaced with hasattr(item, "get") + added test with Pydantic-like objects 466b4dd
Duplicate gen_ai.response.finish_reasons block P1 Removed duplicate 9928618
or-chain falsy fallthrough P2 Changed to is not None guards + added falsy-fallthrough test 466b4dd
Missing per-tool-call span attributes P2 Added _tool_calls_kv_pair call for Responses API function_call items + added indexing tests 466b4dd

@krrish-berri-2

Copy link
Copy Markdown
Contributor

@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!

@aneeshsangvikar

Copy link
Copy Markdown
Contributor Author

Verification

Here's the output of calling set_attributes() with a Responses API response object, showing all three previously-missing attributes are now populated:

======================================================================
Responses API (/v1/responses) -- OTel Span Attributes (WITH fix)
======================================================================
  Request model             | gpt-4o-mini
  Provider                  | openai
  Operation                 | responses
  Input tokens              | 25
  Output tokens             | 10
  System instructions       | You are a helpful math tutor.
  Input messages            | [{"role": "user", "parts": [{"type": "text", "content": "What is 2+2?"}]}]
  Output messages           | [{"role": "assistant", "parts": [{"type": "text", "content": "2 + 2 = 4."}]}]
  Finish reasons            | ["completed"]

======================================================================
BEFORE this fix, the last 3 rows would all show MISSING.
======================================================================

Before the fix: gen_ai.system_instructions, gen_ai.output.messages, and gen_ai.response.finish_reasons were never set for /v1/responses calls because set_attributes() only checked response_obj.get("choices") (chat completion format) and kwargs.get("system_instructions") (Gemini path).

After the fix: All three are populated using the Responses API equivalents (output, instructions, status).

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]}")

@aneeshsangvikar

Copy link
Copy Markdown
Contributor Author

@krrish-berri-2 Thanks for reviewing! The verification is in this comment above — it shows the before/after span attributes for a /v1/responses call, along with a self-contained reproduction script you can copy-paste to verify locally.

In short, the three previously-missing attributes are now populated:

Attribute Before After
gen_ai.output.messages MISSING [{"role": "assistant", "parts": [{"type": "text", "content": "2 + 2 = 4."}]}]
gen_ai.system_instructions MISSING You are a helpful math tutor.
gen_ai.response.finish_reasons MISSING ["completed"]

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.
@aneeshsangvikar

Copy link
Copy Markdown
Contributor Author

@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!

Screenshot 2026-04-30 at 11 13 58 AM (2)

@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: Merged into staging branch litellm_agent_oss_staging_05_06_2026. Staging PR: #27256


Triage Summary
Fixes OpenTelemetry span attribute population for the Responses API. Coalesces three different kwarg names that carry the system prompt (system_instructions for Vertex AI Gemini, instructions for OpenAI Responses API, system for Anthropic) into a single resolution path, and handles both plain-string and structured formats. Also adds gen_ai.output.messages support for Responses API output items. Adds 627 lines of new tests in test_opentelemetry.py covering the new code paths.

Merge Confidence: 5/5 ✅ READY
Ready to ship.

All checks green. Greptile 5/5, no blocking pattern findings, no CircleCI runs (OSS-typical).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants