Skip to content

feat(telemetry): emit gen_ai.response.model from Output.metadata["model"]#275

Merged
Kamilbenkirane merged 1 commit into
mainfrom
feat/emit-gen-ai-response-model
May 6, 2026
Merged

feat(telemetry): emit gen_ai.response.model from Output.metadata["model"]#275
Kamilbenkirane merged 1 commit into
mainfrom
feat/emit-gen-ai-response-model

Conversation

@Kamilbenkirane
Copy link
Copy Markdown
Member

Summary

output_attributes now emits gen_ai.response.model set to output.metadata["model"] — the model id celeste already populates on every Output. Closes the GenAI semconv gen_ai.response.model gap.

Why

GenAI semconv defines two distinct attributes:

  • gen_ai.request.model — the id the caller requested
  • gen_ai.response.model — the id the provider actually served

Pre-PR celeste only emitted the request side. For routing/aliasing providers (Anthropic latest-tag → dated snapshot, OpenAI alias → snapshot) the response side can differ, and dashboards need it to attribute cost/latency/errors to the actually-served variant.

Mechanism

One-line change in output_attributes:

attrs["gen_ai.response.model"] = output.metadata["model"]

metadata["model"] is universally populated:

  • Non-streaming: ModalityClient._build_metadata writes it (client.py:415).
  • Streaming: client.py:_stream passes stream_metadata={"model": self.model.id, ...} to the Stream constructor; Stream._build_stream_metadata spreads it into the Output's metadata.

For providers that don't echo a different served model, the emitted attribute equals gen_ai.request.model. That's accurate for the common case where served = requested.

Why no provider mixin changes

Provider mixins COULD overwrite metadata["model"] with the response's served value (Anthropic response_data["model"], Gemini modelVersion, etc.) to surface routing differences. They don't today; this PR is intentionally minimal. Aliasing-provider users who want the served snapshot can override _build_metadata in their provider mixin in a follow-up — the data flow is already there.

Tests

  • TelemetryStream test fixture defaults stream_metadata={"model": "test-model"} to mirror production wiring.
  • test_off_spec_usage_emitted_under_celeste_namespace updated to construct Output with metadata={"model": ...}.
  • New test_response_model_emitted_from_metadata asserts the attribute is read from metadata correctly.

631 tests pass. Real-call validated against running chat-backend (Gemini text streaming + non-streaming + image-gen): every GenAI span carries gen_ai.response.model.

Out of scope

  • Provider-side overwrite of metadata["model"] with the served snapshot. Easy follow-up; doesn't belong in this minimal landing.
  • Adding Output.model as a typed field. metadata["model"] is the existing surface; promoting it would expand the public Output API for one optional string.

Test plan

  • make lint, make format --check, make typecheck — clean.
  • uv run pytest tests/unit_tests/ -m "not integration" -q — 631 passed.
  • Real-call: Gemini text streaming + non-streaming + image-gen — gen_ai.response.model present on every span.

…el"]

`output_attributes` now emits `gen_ai.response.model` set to
`output.metadata["model"]` — the model id celeste already populates on
every Output. Both `_predict` (via `ModalityClient._build_metadata`) and
`_stream` (via the `stream_metadata` constructor arg) write `model` to
metadata, so the field is universally present; no guards needed.

For providers that echo a different served snapshot in their response
(Anthropic latest-tag → dated, OpenAI alias → snapshot), provider mixins
can opt in to overwrite `metadata["model"]` with the served value. None
do that today — `metadata["model"]` is the request id everywhere — and
the emitted attribute equals `gen_ai.request.model` in practice. That's
semconv-clean for the common case where served = requested.

Test fixture `TelemetryStream` now defaults `stream_metadata={"model":
"test-model"}` to mirror production wiring; one updated test passes the
metadata explicitly. New positive test asserts `gen_ai.response.model`
reads from `metadata["model"]`.

Real-call validated against running chat-backend (Gemini text + image
streaming): every GenAI span now carries `gen_ai.response.model`.
@Kamilbenkirane Kamilbenkirane merged commit a1bd78b into main May 6, 2026
11 checks passed
@claude
Copy link
Copy Markdown

claude Bot commented May 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

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.

1 participant