Skip to content

Instrument OpenAI Responses.retrieve and AsyncResponses.retrieve - #184

Open
JacksonWeber wants to merge 21 commits into
open-telemetry:mainfrom
JacksonWeber:openai-responses-retrieve-instrumentation
Open

Instrument OpenAI Responses.retrieve and AsyncResponses.retrieve#184
JacksonWeber wants to merge 21 commits into
open-telemetry:mainfrom
JacksonWeber:openai-responses-retrieve-instrumentation

Conversation

@JacksonWeber

@JacksonWeber JacksonWeber commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Adds instrumentation for the OpenAI Responses retrieve API (sync and async), addressing the OpenAI item in #141.

Responses.retrieve / AsyncResponses.retrieve fetch a stored response by id without going through create(), so the existing create wrappers don't cover them. Both are now wrapped (reusing the create wrappers, since the result is the same Response shape) and unwrapped on uninstrument.

Note: retrieve only takes a response_id, so the span has no gen_ai.request.model — all attributes come from the fetched response.

Tests: sync/async basic + content-capture cases with cassettes.

The retrieve cassettes were synthesized without provider access, so per CONTRIBUTING.md they carry a # TODO: this is generated by AI, re-record marker. Re-recording is tracked in #341.

Refs #141

These fetch a stored response by id without going through create(), so the existing create wrappers don't cover them. Wrap both sync and async, reusing the create wrappers since the result is the same Response shape. Add sync/async tests and cassettes.

Refs open-telemetry#141

Assisted-by: Claude Opus 4.8

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JacksonWeber and others added 2 commits June 29, 2026 19:22
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the 404 NotFoundError path for sync and async retrieve, matching the happy-path-plus-error coverage of the other Responses operations.

Assisted-by: Claude Opus 4.8

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@JacksonWeber
JacksonWeber marked this pull request as ready for review June 30, 2026 02:42
@JacksonWeber
JacksonWeber requested a review from a team as a code owner June 30, 2026 02:42
Copilot AI review requested due to automatic review settings June 30, 2026 02:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds OpenAI Responses retrieve() coverage to the existing opentelemetry-instrumentation-genai-openai package so stored responses fetched by ID emit the same GenAI telemetry as create(), including content-capture behavior and error recording.

Changes:

  • Wrap openai.resources.responses.responses.Responses.retrieve and AsyncResponses.retrieve using the existing responses_create / async_responses_create wrappers.
  • Add sync + async unit tests for retrieve() covering basic attributes, content capture, and 404 errors (with new VCR cassettes).
  • Add a towncrier changelog fragment documenting the new API coverage.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/init.py Instruments/uninstruments Responses.retrieve and AsyncResponses.retrieve using existing response wrappers.
instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_responses.py Adds sync retrieve() tests for basic attributes, content capture, and API error behavior.
instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_async_responses.py Adds async retrieve() tests for basic attributes, content capture, and API error behavior (explicit cassette usage).
instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_retrieve_basic[content_mode0].yaml VCR cassette for sync retrieve() happy path (no content capture).
instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_retrieve_captures_content[content_mode0].yaml VCR cassette for sync retrieve() happy path with output content capture.
instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_retrieve_api_error[content_mode0].yaml VCR cassette for sync retrieve() 404 / NotFoundError path.
instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_retrieve_basic[content_mode0].yaml VCR cassette for async retrieve() happy path (no content capture).
instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_retrieve_captures_content[content_mode0].yaml VCR cassette for async retrieve() happy path with output content capture.
instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_retrieve_api_error[content_mode0].yaml VCR cassette for async retrieve() 404 / NotFoundError path.
instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/184.added Changelog fragment for the new retrieve() instrumentation coverage.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Jun 30, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on reviewers · refreshed 2026-08-06 03:12 UTC

Review the latest changes.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@github-actions

Copy link
Copy Markdown

This PR has been automatically marked as stale because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 days of this comment.
If you're still working on this, please add a comment or push new commits.

@github-actions github-actions Bot added the Stale Issue or PR has been inactive label Jul 15, 2026
@lmolkova lmolkova removed the Stale Issue or PR has been inactive label Jul 15, 2026
JacksonWeber and others added 2 commits July 31, 2026 14:46
Resolve conflict in the OpenAI instrumentor: keep the Responses.stream /
AsyncResponses.stream unwraps from main alongside the retrieve unwraps, and
update the retrieve error tests for the fully-qualified openai.* error.type
values now emitted by util-genai.

Assisted-by: Claude Opus 5
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The six retrieve cassettes reuse the CF-RAY, x-request-id and Date headers of
the existing responses create cassettes, so they were synthesized rather than
recorded against the provider. AGENTS.md requires such cassettes to carry the
re-record marker.

Assisted-by: Claude Opus 5
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@opentelemetry-pr-dashboard

Copy link
Copy Markdown

Hi @JacksonWeber — just a friendly reminder that this pull request is waiting on you.

There are still items that need your attention. See the dashboard status comment for the full list. You don't need to push a code change to hand it back — replying to move each discussion forward is enough, whether that's answering a question, explaining why no change is needed, or asking a follow-up. The dashboard then automatically routes it back to reviewers.

If you believe this pull request is incorrectly routed as waiting on the author, comment /dashboard route:reviewers to route it from waiting on the author to waiting on reviewers.

JacksonWeber and others added 3 commits August 4, 2026 11:10
Adds a FetchResponseInvocation to opentelemetry-util-genai and wires the OpenAI
Responses retrieve instrumentation to it, so fetching a stored response is no
longer reported as an inference operation and does not record token usage.

Assisted-by: Claude Opus 5
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A fetched response carries the tool definitions the original generation was
made with, and semconv lists gen_ai.tool.definitions as opt-in content on the
fetch_response span, but set_fetch_response_attributes never recorded them.

Responses API tools are flat, so the Chat Completions helper
_prepare_tool_definitions() does not apply: it reads tool.function.name and
would return nothing here. Add a Responses-specific extractor that maps
function tools onto FunctionToolDefinition and built-in tools, which carry no
name, onto GenericToolDefinition keyed by their type.

Assisted-by: Claude Opus 5
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +63 to +89
def _is_streamed_raw_response(kwargs: dict[str, Any]) -> bool:
extra_headers = kwargs.get("extra_headers")
if not isinstance(extra_headers, Mapping):
return False
return any(
key.lower() == _RAW_RESPONSE_HEADER and value == "stream"
for key, value in extra_headers.items()
)


def _parse_raw_response(result: object, kwargs: dict[str, Any]) -> object:
"""Return the payload of a non-streaming ``with_raw_response`` result.

A non-streaming raw response has already read its body and the SDK memoizes
``parse()``, so deserializing it here does not change what the caller sees.
On the async client ``parse()`` returns an awaitable, which the caller
awaits.

``with_streaming_response`` hands back a raw response whose body has *not*
been read yet; parsing it would consume the body before the caller asks for
it, so those calls are left alone.
"""
if _is_streamed_raw_response(kwargs) or not isinstance(
result, ParsableResponse
):
return result
return result.parse()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have it already for responses -

, let's reuse it

it seems _is_streamed_raw_response would be useful there too btw.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved response detection/parsing to response_extractors.py and now reuse it for inference and fetch-response extraction.


parsed = _parse_raw_response(result, kwargs)
if inspect.isawaitable(parsed):
parsed = await parsed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in response_extractors.py

}


def get_fetched_finish_reasons(response: "Response | None") -> list[str]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we align fetch and responses create finish reasons - there is a related helper above - _finish_reason_from_status and responses.create also returns status. we should use the same helpers for both

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create and fetch now use the same logic.

**get_fetch_response_creation_kwargs(response_id, instance)
)
invocation.stream_cursor = _get_stream_cursor(kwargs)
invocation.attributes[OpenAIAttributes.OPENAI_API_TYPE] = (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! we should set it on responses.create too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've set this value on all Responses now


def _get_attributes(self) -> dict[str, AttributeValue]:
optional_attrs: tuple[tuple[str, AttributeValue | None], ...] = (
(_GEN_AI_REQUEST_STREAM_CURSOR, self.stream_cursor),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also set gen_ai.request.stream when there was streaming

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TelemetryHandler.fetch_resopnse() now accepts if the request is streamed and records it at span creation.

# retrieve() fetches a stored response by id. No inference happens
# and no tokens are consumed, so it is traced as a fetch_response
# operation rather than through the create wrappers.
if hasattr(responses_module.Responses, "retrieve"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is hasattr(responses_module.Responses, "retrieve"): necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've confirmed the hasattr check here is unnecessary since _get_responses_module() already gates the block.

JacksonWeber and others added 9 commits August 5, 2026 14:05
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Sol
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot CLI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants