Repository navigation
Description
Which component is this bug for?
Google Generative AI Instrumentation
📜 Description
The OpenTelemetry GenAI semantic conventions make gen_ai.client.operation.duration a required metric, with error.type conditionally required "if the operation ended in an error", so every failed call is expected to produce a duration point with error.type. Several OpenLLMetry instrumentations miss failures:
| Package | Failure | What is recorded |
|---|---|---|
| google-generativeai | generate_content fails (HTTP 500) |
no duration point |
| google-generativeai | streaming call fails | duration point without error.type |
| bedrock | invoke_model / converse (and streaming variants) fail |
no duration point |
| anthropic | stream fails mid-way (sync and async) | no duration point |
| openai | chat stream fails mid-way (sync and async) | duration point without error.type |
| litellm | completion / embedding fails (HTTP 500), or a stream fails |
no duration point |
For comparison, a request that fails before streaming is recorded correctly by the OpenAI and Anthropic instrumentations (one point with error.type), and the OpenTelemetry GenAI instrumentations record a point with error.type in all of the cases above.
Code paths (0.62.4):
- google-generativeai
_wrap/_awrap(__init__.py, lines 340 and 261): theexceptblock records the exception on the span, ends it and re-raises; the duration is only recorded after a successful call. - bedrock
_instrumented_model_invoke,_instrumented_converseand the streaming/async variants (e.g.__init__.pyline 239): theexceptblock setserror.typeon the span and re-raises; the duration is recorded later in_handle_call, which a failed call never reaches. The only duration-with-error.typecode (around line 205) wraps client creation, not model calls. - anthropic stream
__next__/__anext__(streaming.py, lines 250 and 382): on an exception they add to the exception counter and re-raise; the duration is only recorded in_complete_instrumentation, after a successful end. - openai
ChatStream.__next__/__anext__(shared/chat_wrappers.py, line 734): on an exception_ensure_cleanup()->_record_partial_metrics()records the duration with_shared_attributes()only, withouterror.type. - litellm
_handle_exception(__init__.py, line 630) only records the exception counter;_LiteLLMStream._finish_errorlikewise.
As a result, error-rate and latency views built on gen_ai.client.operation.duration miss or misclassify these failures.
👟 Reproduction steps
Google GenAI and Bedrock (fail before any response):
import json
import httpx, boto3
from botocore.awsrequest import AWSResponse
from botocore.config import Config
from google import genai
from google.genai import types
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.google_generativeai import GoogleGenerativeAiInstrumentor
from opentelemetry.instrumentation.bedrock import BedrockInstrumentor
reader = InMemoryMetricReader()
tp, mp = TracerProvider(), MeterProvider(metric_readers=[reader])
GoogleGenerativeAiInstrumentor().instrument(tracer_provider=tp, meter_provider=mp)
BedrockInstrumentor().instrument(tracer_provider=tp, meter_provider=mp)
def duration_points():
data = reader.get_metrics_data()
return sum(p.count for rm in (data.resource_metrics if data else []) for sm in rm.scope_metrics
for m in sm.metrics if m.name == "gen_ai.client.operation.duration" for p in m.data.data_points)
def check(label, call):
before = duration_points()
try:
call(); outcome = "ok"
except Exception as e:
outcome = type(e).__name__
print(f"{label:28} {outcome:24} new duration points: {duration_points() - before}")
gclient = genai.Client(api_key="x", http_options=types.HttpOptions(httpx_client=httpx.Client(
transport=httpx.MockTransport(lambda r: httpx.Response(
500, json={"error": {"code": 500, "message": "boom", "status": "INTERNAL"}})))))
check("google generate_content", lambda: gclient.models.generate_content(model="gemini-2.0-flash", contents="hello"))
class Raw:
def __init__(self, d): self.d = d
def stream(self, *a, **k): yield self.d
def read(self, amt=None): return self.d
def close(self): pass
release_conn = close
bclient = boto3.client("bedrock-runtime", region_name="us-east-1", aws_access_key_id="x",
aws_secret_access_key="x", config=Config(retries={"total_max_attempts": 1}))
bclient.meta.events.register("before-send.bedrock-runtime.*", lambda request, **kw: AWSResponse(
request.url, 500, {"content-type": "application/json", "x-amzn-errortype": "InternalServerException"},
Raw(json.dumps({"message": "boom"}).encode())))
model = "anthropic.claude-3-haiku-20240307-v1:0"
check("bedrock invoke_model", lambda: bclient.invoke_model(modelId=model, body=json.dumps(
{"anthropic_version": "bedrock-2023-05-31", "max_tokens": 50, "messages": [{"role": "user", "content": "hi"}]})))
check("bedrock converse", lambda: bclient.converse(modelId=model,
messages=[{"role": "user", "content": [{"text": "hi"}]}]))OpenAI and Anthropic (the stream breaks after its first event):
import json
import httpx, openai, anthropic
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
reader = InMemoryMetricReader()
tp, mp = TracerProvider(), MeterProvider(metric_readers=[reader])
OpenAIInstrumentor().instrument(tracer_provider=tp, meter_provider=mp)
AnthropicInstrumentor().instrument(tracer_provider=tp, meter_provider=mp)
def duration_points():
out = {}
data = reader.get_metrics_data()
for rm in (data.resource_metrics if data else []):
for sm in rm.scope_metrics:
for m in sm.metrics:
if m.name == "gen_ai.client.operation.duration":
for p in m.data.data_points:
k = p.attributes.get("error.type") or "no error.type"
out[k] = out.get(k, 0) + p.count
return out
def broken(hx, first): # sends the first SSE event, then the connection breaks
class S(hx.SyncByteStream):
def __iter__(self):
yield first
raise hx.ReadError("connection broken")
return hx.MockTransport(lambda r: hx.Response(200, headers={"content-type": "text/event-stream"}, stream=S()))
def check(label, call):
before = duration_points()
try:
call(); outcome = "ok"
except Exception as e:
outcome = type(e).__name__
after = duration_points()
print(label, outcome, {k: c - before.get(k, 0) for k, c in after.items() if c != before.get(k, 0)} or "NONE")
chunk = {"id": "c1", "object": "chat.completion.chunk", "created": 0, "model": "gpt-4o-mini",
"choices": [{"index": 0, "delta": {"role": "assistant", "content": "hi"}, "finish_reason": None}]}
oc = openai.OpenAI(api_key="x", max_retries=0, http_client=httpx.Client(
transport=broken(httpx, f"data: {json.dumps(chunk)}\n\n".encode())))
check("openai chat stream fails mid-way:", lambda: list(oc.chat.completions.create(
model="gpt-4o-mini", messages=[{"role": "user", "content": "hello"}], stream=True)))
import anthropic._base_client as bc
hx = getattr(bc, "httpx2", None) or httpx # newer anthropic SDKs use httpx2
start = {"type": "message_start", "message": {"id": "m1", "type": "message", "role": "assistant", "model": "claude-x",
"content": [], "stop_reason": None, "stop_sequence": None, "usage": {"input_tokens": 7, "output_tokens": 0}}}
ac = anthropic.Anthropic(api_key="x", max_retries=0, http_client=hx.Client(
transport=broken(hx, f"event: message_start\ndata: {json.dumps(start)}\n\n".encode())))
check("anthropic stream fails mid-way:", lambda: list(ac.messages.create(
model="claude-x", max_tokens=50, messages=[{"role": "user", "content": "hello"}], stream=True)))LiteLLM (HTTP 500):
import os
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
import httpx, litellm, openai
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.litellm import LiteLLMInstrumentor
reader = InMemoryMetricReader()
LiteLLMInstrumentor().instrument(tracer_provider=TracerProvider(), meter_provider=MeterProvider(metric_readers=[reader]))
def duration_points():
data = reader.get_metrics_data()
return sum(p.count for rm in (data.resource_metrics if data else []) for sm in rm.scope_metrics
for m in sm.metrics if m.name == "gen_ai.client.operation.duration" for p in m.data.data_points)
client = openai.OpenAI(api_key="x", max_retries=0, http_client=httpx.Client(transport=httpx.MockTransport(
lambda r: httpx.Response(500, json={"error": {"message": "boom", "type": "server_error"}}))))
for label, call in (("litellm completion", lambda: litellm.completion(model="openai/gpt-4o-mini", client=client, api_key="x",
messages=[{"role": "user", "content": "hello"}])),
("litellm embedding", lambda: litellm.embedding(model="openai/text-embedding-3-small", client=client,
api_key="x", input=["hello"]))):
before = duration_points()
try:
call(); outcome = "ok"
except Exception as e:
outcome = type(e).__name__
print(f"{label:28} {outcome:24} new duration points: {duration_points() - before}")👍 Expected behavior
Every call, successful or not, records one gen_ai.client.operation.duration point, and a failed call (including a stream that fails mid-way) carries error.type (e.g. ServerError, InternalServerException, ReadError), as these instrumentations already do for requests that fail before streaming.
👎 Actual Behavior with Screenshots
Google GenAI (google-genai 2.29.0, opentelemetry-instrumentation-google-generativeai 0.62.4):
models.generate_content ok duration points: +1 (no error.type)
models.generate_content ServerError duration points: NONE
models.generate_content_stream ok duration points: +1 (no error.type)
models.generate_content_stream ServerError duration points: +1 (no error.type)
Bedrock (boto3 1.43.111, opentelemetry-instrumentation-bedrock 0.62.4):
invoke_model ok duration points: +1 (no error.type)
invoke_model InternalServerException duration points: NONE
converse ok duration points: +1 (no error.type)
converse InternalServerException duration points: NONE
OpenAI chat.completions.create(stream=True) (openai 2.54.0, opentelemetry-instrumentation-openai 0.62.4):
sync fails before streaming (HTTP 500) InternalServerError duration points: +1 (InternalServerError)
async fails before streaming (HTTP 500) InternalServerError duration points: +1 (InternalServerError)
sync stream fails mid-way ReadError duration points: +1 (no error.type)
async stream fails mid-way ReadError duration points: +1 (no error.type)
Anthropic messages.create(stream=True) (anthropic 1.13.0, opentelemetry-instrumentation-anthropic 0.62.4):
sync fails before streaming (HTTP 500) InternalServerError duration points: +1 (InternalServerError)
async fails before streaming (HTTP 500) InternalServerError duration points: +1 (InternalServerError)
sync stream fails mid-way ReadError duration points: NONE
async stream fails mid-way ReadError duration points: NONE
LiteLLM (litellm 1.104.2, opentelemetry-instrumentation-litellm 0.62.4):
completion, stream whose connection drops MidStreamFallbackError duration points: NONE
completion rejected by the server (HTTP 500) InternalServerError duration points: NONE
🤖 Python Version
3.12
📃 Provide any additional context for the Bug.
All five packages at 0.62.4 (the latest release), with the SDK versions listed above.
👀 Have you spent some time to check if this bug has been raised before?
- I checked and didn't find similar issue
Are you willing to submit PR?
None
Activity
shantam77 commentedon Oct 11, 2026
I would like to work on this.
I just worked in that package for #4607
armanunix commentedon Oct 11, 2026
The same in opentelemetry-instrumentation-ollama 0.62.4 (ollama 0.6.3): a failed
chat/generaterecords nogen_ai.client.operation.durationpoint, while a successful one records one. In_wrap/_awrapthe duration is only recorded after a successfulwrapped(...)call; the open PR #4127 sets the error status on the span but still records no duration for the failure.jay0237 commentedon Oct 13, 2026
Hello maintainers! It would be great to work on the Ollama-specific part of the issue.
I have created a local patch for adding a metric
gen_ai.client.operation.durationto failed synchronous and asynchronouschatandgeneratecalls. Also, I have added regression tests for the successful case, failing case, and stream.Ollama package tests and Ruff linter are passed locally. I have seen that there is another developer who wants to fix this problem too, so I wanted to coordinate before creating the PR.
Is it possible for me to create a PR for the Ollama-specific part of the issue or maybe somebody else is working on this?