Instrument OpenAI Responses.retrieve and AsyncResponses.retrieve - #184
Instrument OpenAI Responses.retrieve and AsyncResponses.retrieve#184JacksonWeber wants to merge 21 commits into
Conversation
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>
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>
There was a problem hiding this comment.
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.retrieveandAsyncResponses.retrieveusing the existingresponses_create/async_responses_createwrappers. - 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. |
Pull request dashboard statusWaiting on reviewers · refreshed 2026-08-06 03:12 UTC Review the latest changes. Status above doesn't look right?
|
|
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. |
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>
|
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 |
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>
| 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() |
There was a problem hiding this comment.
we should have it already for responses -
it seems _is_streamed_raw_response would be useful there too btw.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
this is probably also necessary in
There was a problem hiding this comment.
Added in response_extractors.py
| } | ||
|
|
||
|
|
||
| def get_fetched_finish_reasons(response: "Response | None") -> list[str]: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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] = ( |
There was a problem hiding this comment.
nice! we should set it on responses.create too
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
we should also set gen_ai.request.stream when there was streaming
There was a problem hiding this comment.
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"): |
There was a problem hiding this comment.
is hasattr(responses_module.Responses, "retrieve"): necessary?
There was a problem hiding this comment.
I've confirmed the hasattr check here is unnecessary since _get_responses_module() already gates the block.
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>
Adds instrumentation for the OpenAI Responses
retrieveAPI (sync and async), addressing the OpenAI item in #141.Responses.retrieve/AsyncResponses.retrievefetch a stored response by id without going throughcreate(), so the existing create wrappers don't cover them. Both are now wrapped (reusing the create wrappers, since the result is the sameResponseshape) and unwrapped on uninstrument.Note:
retrieveonly takes aresponse_id, so the span has nogen_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-recordmarker. Re-recording is tracked in #341.Refs #141