Telemetry for a custom adapter (agent guide)
This is a self-contained guide for making a custom model adapter telemetry-friendly. It's written so you can hand it straight to a coding agent — the last section is a copy-paste brief.
The short version
A custom adapter needs no extra code to be traced. When
telemetry is enabled, runspec-console creates the spans
— one trace per agent turn, turn → llm → tool — around your adapter's
stream_with_tools call, and sets the standard OpenTelemetry GenAI attributes
itself. An adapter that simply satisfies the ModelAdapter contract is fully
traced without knowing telemetry exists.
There are three tiers of involvement; only the third is optional adapter work:
| Tier | What | Adapter work |
|---|---|---|
| 1. Tracing works | Spans, tree, model name, durations | None |
| 2. Accurate tokens / finish reason | LLM span's gen_ai.usage.* + finish_reasons |
Already part of the adapter contract — surface usage on the response |
| 3. Provider-specific detail | Extra span attributes, prompt/completion text | Optional, no-op-safe helpers |
Tier 2 — surface usage on the response (already the contract)
The console reads token counts and the finish reason off the ChatResponse your
adapter returns. For the LLM span's gen_ai.usage.input_tokens /
output_tokens (and the cache split) to be populated, the response's _raw
must expose a .usage with the provider's token fields, and stop_reason must
be set. This is normal adapter behaviour, not telemetry-specific — if usage
isn't present, the span still forms correctly and the token attributes just read
0 (no error).
Confirm the exact field names against the installed package rather than
guessing (see the brief below) — the console's usage reader accepts the common
spellings (input_tokens/prompt_tokens, output_tokens/completion_tokens,
cache_read_input_tokens, cache_creation_input_tokens).
Tier 3 — enrich the live span (optional)
While your adapter's stream_with_tools runs, the LLM span is the current
OpenTelemetry span. So an adapter can add provider-specific detail — a gateway
request id, a response id — or, when the operator has turned on
capture_content, the completion text, by calling the helpers on
runspec_console.observability:
from runspec_console import observability
# inside the adapter, after the provider responds:
observability.set_attributes(**{
"gen_ai.response.id": response_id,
"mycorp.gateway.request_id": request_id,
})
observability.add_completion(text) # honours capture_content; no-op when off
Every helper is a complete no-op when telemetry is disabled or the otel
extra isn't installed, so call them unconditionally — your adapter never needs
opentelemetry as a dependency, and this keeps ChatResponse SDK-free
(enrichment is a side channel, not a new response field).
Requirements
The observability enrichment export requires runspec-console ≥ 0.218.0.
The reliable capability check is simply that the import succeeds:
from runspec_console import observability # only present on ≥ 0.218.0
Copy-paste brief for an agent
Task: make our custom runspec-console model adapter telemetry-friendly.
Read this page and https://runspec.app/console-telemetry/ first.
runspec-consoleis pip-installed; verify the real API by introspecting the installed package (don't code against a paraphrase):python - <<'PY' import inspect from runspec_console import observability # public top-level export from runspec_console.adapters.base import ChatResponse, ToolCall, ModelAdapter from runspec_console.bridge_invoke import InvokeMixin for fn in (observability.current_span, observability.set_attributes, observability.add_prompt, observability.add_completion): print(fn.__name__, inspect.signature(fn)) print(inspect.getsource(ChatResponse)) print(inspect.getsource(InvokeMixin._usage_from_response)) # exact usage fields PY # To read the files instead: # python -c "import runspec_console,os;print(os.path.dirname(runspec_console.__file__))" # then open observability.py, adapters/base.py, bridge_invoke.py there.Rules:
- Telemetry (OTLP tracing) needs no adapter changes to function — the console creates the
turn → llm → toolspans aroundstream_with_tools. Do not add span-creation code.- Ensure the adapter returns a
ChatResponsewhose_raw.usageexposesinput_tokensandoutput_tokens(pluscache_read_input_tokens/cache_creation_input_tokensif the provider reports them), and thatstop_reasonis correct ("tool_use"when tool calls are present; a length spelling like"max_tokens"on a cutoff; else"end_turn"). Confirm the exact field names from_usage_from_responseabove. If usage isn't on_raw.usage, token attributes silently read0(no error) — fix only that.- (Optional) For provider-specific span detail, or completion/prompt text when
capture_contentis enabled, call the no-op-safe helpers unconditionally:from runspec_console import observabilitythenobservability.set_attributes(**{...})/observability.add_completion(text). Never importopentelemetrydirectly and add no new dependency — the helpers are no-ops when telemetry is off or theotelextra isn't installed.- Adapter code only — don't touch console config or the span wiring.
Requires runspec-console ≥ 0.218.0; treat
from runspec_console import observabilityimporting successfully as the capability check.
See also
- Custom model adapters — the full adapter contract.
- Telemetry — OpenTelemetry tracing — enabling tracing, backends, and the span/attribute map.