Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f839c29
feat(gemini): migrate vertexai and google-generativeai to OTel GenAI …
avivhalfon Mar 22, 2026
c13d0e2
fix(google-generativeai): remove unused SpanAttributes import
avivhalfon Mar 23, 2026
a61054f
fix
OzBenSimhonTraceloop Mar 29, 2026
22c8e67
merge origin/main into ah/gemini-sem-conv
OzBenSimhonTraceloop Mar 29, 2026
8683440
last-fix
OzBenSimhonTraceloop Mar 29, 2026
3c24e3c
coderabbit
OzBenSimhonTraceloop Mar 29, 2026
8548ce2
Merge branch 'main' of github.com:traceloop/openllmetry into ah/gemin…
OzBenSimhonTraceloop Mar 30, 2026
3d67642
fix(google-generativeai, vertexai): OTel semconv compliance fixes
OzBenSimhonTraceloop Mar 30, 2026
0f352b3
revert: remove event emitter changes (pre-existing, not in branch scope)
OzBenSimhonTraceloop Mar 30, 2026
f58b77d
fix: revert BlobPart field name — OTel spec uses "content", not "data"
OzBenSimhonTraceloop Mar 30, 2026
e99a6eb
Merge branch 'main' of github.com:traceloop/openllmetry into ah/gemin…
OzBenSimhonTraceloop Mar 30, 2026
4a21396
wip
OzBenSimhonTraceloop Mar 31, 2026
ce00bb5
cleanup
OzBenSimhonTraceloop Mar 31, 2026
2a6da1a
more-test
OzBenSimhonTraceloop Mar 31, 2026
7d013af
empty
OzBenSimhonTraceloop Mar 31, 2026
63aa0a2
fix
OzBenSimhonTraceloop Mar 31, 2026
07d03cc
revert: remove vertex ai semconv changes from branch
OzBenSimhonTraceloop Apr 5, 2026
501e3b6
fix
OzBenSimhonTraceloop Apr 5, 2026
13ee5e9
gemini-sample-app-model
OzBenSimhonTraceloop Apr 6, 2026
2d7cd0b
dead-code
OzBenSimhonTraceloop Apr 6, 2026
723b8e6
Revert "dead-code"
OzBenSimhonTraceloop Apr 6, 2026
e809593
fix-async
OzBenSimhonTraceloop Apr 6, 2026
4b04b86
wip
OzBenSimhonTraceloop Apr 6, 2026
5106246
lint
OzBenSimhonTraceloop Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
emit_message_events,
)
from opentelemetry.instrumentation.google_generativeai.span_utils import (
_collect_finish_reasons_from_response,
set_input_attributes,
set_input_attributes_sync,
set_model_request_attributes,
set_model_response_attributes,
Expand All @@ -32,40 +34,37 @@
)
from opentelemetry.semconv_ai import (
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
LLMRequestTypeValues,
SpanAttributes,
Meters
Meters,
)
from opentelemetry.metrics import Meter, get_meter
from opentelemetry.trace import SpanKind, get_tracer, StatusCode
from wrapt import wrap_function_wrapper

_GCP_GEN_AI = GenAIAttributes.GenAiProviderNameValues.GCP_GEN_AI.value
_GEN_CONTENT = GenAIAttributes.GenAiOperationNameValues.GENERATE_CONTENT.value

logger = logging.getLogger(__name__)

WRAPPED_METHODS = [
{
"package": "google.genai.models",
"object": "Models",
"method": "generate_content",
"span_name": "gemini.generate_content",
},
{
"package": "google.genai.models",
"object": "AsyncModels",
"method": "generate_content",
"span_name": "gemini.generate_content",
},
{
"package": "google.genai.models",
"object": "Models",
"method": "generate_content_stream",
"span_name": "gemini.generate_content",
},
{
"package": "google.genai.models",
"object": "AsyncModels",
"method": "generate_content_stream",
"span_name": "gemini.generate_content",
},
]

Expand All @@ -85,43 +84,79 @@ def _build_from_streaming_response(
event_logger,
token_histogram,
):
complete_response = ""
emit_events = should_emit_events() and event_logger
text_parts = []
last_chunk = None
for item in response:
item_to_yield = item
last_chunk = item
complete_response += str(item.text)
if not emit_events:
t = getattr(item, "text", None)
if isinstance(t, str):
text_parts.append(t)

yield item_to_yield

if should_emit_events() and event_logger:
complete_response = "".join(text_parts)

if emit_events:
emit_choice_events(response, event_logger)
else:
set_response_attributes(span, complete_response, llm_model)
if last_chunk is not None and getattr(last_chunk, "candidates", None):
set_response_attributes(span, last_chunk, llm_model)
else:
set_response_attributes(
span, complete_response, llm_model, stream_last_chunk=last_chunk
)

# Finish reasons from the final chunk — Gemini SDK aggregates candidates per chunk,
# so the last chunk reflects all candidates without deduplication artifacts.
stream_reasons = _collect_finish_reasons_from_response(last_chunk) if last_chunk else None
set_model_response_attributes(
span, last_chunk or response, llm_model, token_histogram
span,
last_chunk or response,
llm_model,
token_histogram,
stream_finish_reasons=stream_reasons or None,
)
span.end()


async def _abuild_from_streaming_response(
span, response: GenerateContentResponse, llm_model, event_logger, token_histogram
):
complete_response = ""
emit_events = should_emit_events() and event_logger
text_parts = []
last_chunk = None
async for item in response:
item_to_yield = item
last_chunk = item
complete_response += str(item.text)
if not emit_events:
t = getattr(item, "text", None)
if isinstance(t, str):
text_parts.append(t)

yield item_to_yield

if should_emit_events() and event_logger:
complete_response = "".join(text_parts)

if emit_events:
emit_choice_events(response, event_logger)
else:
set_response_attributes(span, complete_response, llm_model)
if last_chunk is not None and getattr(last_chunk, "candidates", None):
set_response_attributes(span, last_chunk, llm_model)
else:
set_response_attributes(
span, complete_response, llm_model, stream_last_chunk=last_chunk
)

stream_reasons = _collect_finish_reasons_from_response(last_chunk) if last_chunk else None
set_model_response_attributes(
span, last_chunk if last_chunk else response, llm_model, token_histogram
span,
last_chunk if last_chunk else response,
llm_model,
token_histogram,
stream_finish_reasons=stream_reasons or None,
)
span.end()

Expand All @@ -136,6 +171,16 @@ def _handle_request(span, args, kwargs, llm_model, event_logger):
set_model_request_attributes(span, kwargs, llm_model)


@dont_throw
async def _handle_request_async(span, args, kwargs, llm_model, event_logger):
if should_emit_events() and event_logger:
emit_message_events(args, kwargs, event_logger)
else:
await set_input_attributes(span, args, kwargs, llm_model)

set_model_request_attributes(span, kwargs, llm_model)


@dont_throw
def _handle_response(span, response, llm_model, event_logger, token_histogram):
if should_emit_events() and event_logger:
Expand Down Expand Up @@ -200,17 +245,17 @@ async def _awrap(
if "model" in kwargs:
llm_model = kwargs["model"].replace("models/", "")

name = to_wrap.get("span_name")
span = tracer.start_span(
name,
f"{_GEN_CONTENT} {llm_model}",
kind=SpanKind.CLIENT,
attributes={
GenAIAttributes.GEN_AI_SYSTEM: "Google",
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
start_time = time.perf_counter()
_handle_request(span, args, kwargs, llm_model, event_logger)
await _handle_request_async(span, args, kwargs, llm_model, event_logger)
try:
response = await wrapped(*args, **kwargs)
except Exception as e:
Expand All @@ -224,7 +269,9 @@ async def _awrap(
duration_histogram.record(
duration,
attributes={
GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google",
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -276,13 +323,13 @@ def _wrap(
if "model" in kwargs:
llm_model = kwargs["model"].replace("models/", "")

name = to_wrap.get("span_name")
span = tracer.start_span(
name,
f"{_GEN_CONTENT} {llm_model}",
kind=SpanKind.CLIENT,
attributes={
GenAIAttributes.GEN_AI_SYSTEM: "Google",
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
},
)

Expand All @@ -301,7 +348,9 @@ def _wrap(
duration_histogram.record(
duration,
attributes={
GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google",
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.instrumentation.google_generativeai.span_utils import (
_map_gemini_finish_reason,
)

_GCP_GEN_AI = GenAIAttributes.GenAiProviderNameValues.GCP_GEN_AI.value


class Roles(Enum):
Expand All @@ -28,7 +33,7 @@ class Roles(Enum):
VALID_MESSAGE_ROLES = {role.value for role in Roles}
"""The valid roles for naming the message event."""

EVENT_ATTRIBUTES = {GenAIAttributes.GEN_AI_SYSTEM: "gemini"}
EVENT_ATTRIBUTES = {GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI}
"""The attributes to be used for the event."""


Expand Down Expand Up @@ -65,7 +70,7 @@ def emit_choice_events(
"content": [part_to_dict(i) for i in candidate.content.parts],
"role": candidate.content.role,
},
finish_reason=candidate.finish_reason.name,
finish_reason=_map_gemini_finish_reason(candidate.finish_reason),
),
event_logger,
)
Expand Down
Loading
Loading