Skip to content

fix(google-genai): migrate span attrs to gen_ai.input/output.messages (fixes #3515) - #3948

Open
abhyudayareddy wants to merge 16 commits into
traceloop:mainfrom
abhyudayareddy:fix/google-genai-span-attr-migration
Open

fix(google-genai): migrate span attrs to gen_ai.input/output.messages (fixes #3515)#3948
abhyudayareddy wants to merge 16 commits into
traceloop:mainfrom
abhyudayareddy:fix/google-genai-span-attr-migration

Conversation

@abhyudayareddy

@abhyudayareddy abhyudayareddy commented Apr 6, 2026

Copy link
Copy Markdown

Summary

Fixes #3515.

Migrates google_generativeai/span_utils.py from deprecated indexed span attributes to the stable OTel GenAI Semantic Conventions:

  • gen_ai.prompt.{N}.content / gen_ai.prompt.{N}.role -> gen_ai.input.messages (JSON array)
  • gen_ai.completion.{N}.content / gen_ai.completion.{N}.role -> gen_ai.output.messages (JSON array)

The new attributes store a JSON-serialised list of {"role": "...", "parts": [...]} objects, consistent with the format used by the OpenAI instrumentation in this repo.

Changes

  • span_utils.py: Removed _set_prompt_attributes; set_input_attributes and set_input_attributes_sync now build an input_messages list and write it to gen_ai.input.messages; set_response_attributes builds an output_messages list and writes it to gen_ai.output.messages.
  • tests/test_generate_content.py: Updated assertions to parse the new JSON blob attributes instead of the old indexed ones.

Checklist

  • I have added tests that cover my changes.
  • If adding a new instrumentation or changing an existing one, I've added screenshots from some observability platform showing the change.
  • PR name follows conventional commits format: fix(instrumentation): ...
  • (If applicable) I have updated the documentation accordingly.

Summary by CodeRabbit

  • Refactor

    • Streamlined input/output handling: inputs are normalized to aggregated JSON messages; inline binary images are base64-encoded and represented as image_url objects for both sync/async paths. Response serialization unified (clear streaming vs non-streaming behavior, finish-reason handling), presence/frequency penalties remapped to LLM_* attributes, and deprecated per-index response attributes removed.
  • Tests

    • Updated to validate aggregated JSON message attributes (roles and part shapes), assert total token usage, and ensure deprecated per-index prompt/completion attributes are not emitted.

@CLAassistant

CLAassistant commented Apr 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Apr 6, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors Google GenAI span serialization to stop emitting deprecated indexed attributes and instead emit structured message arrays gen_ai.input.messages and gen_ai.output.messages. Unifies async/sync image handling (base64 inline), simplifies part processing, rewrites response serialization, and remaps penalty/usage span attributes.

Changes

Cohort / File(s) Summary
Span utils (Google GenAI instrumentation)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
Removed legacy non-image part extractors and deprecated indexed prompt/completion emission. Unified async/sync image handling to base64 inline image objects and returned OpenAI-compatible image_url objects. Replaced multiple part-mapping helpers with a simplified content/part processing pipeline and image helpers. Built a single input_messages array for sync/async flows and set GenAIAttributes.GEN_AI_INPUT_MESSAGES via json.dumps. Rewrote response serialization to build GenAIAttributes.GEN_AI_OUTPUT_MESSAGES (candidates → parts → serialized parts, attach finish_reason when present; streaming omits finish_reason). Remapped request penalty attributes to SpanAttributes.LLM_PRESENCE_PENALTY / SpanAttributes.LLM_FREQUENCY_PENALTY, removed deprecated response-id & finish-reason-array emissions, and gated usage/token histogram recording on response.usage_metadata (using response.usage_metadata.candidates_token_count for output token histogram and labeling provider "Google").
Tests
packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py
Replaced deprecated indexed attribute assertions with checks for GenAIAttributes.GEN_AI_INPUT_MESSAGES and GenAIAttributes.GEN_AI_OUTPUT_MESSAGES (parsed as JSON arrays). Validate message role ("user" for input, "assistant" for output), ensure parts is a non-empty list and first part is { "type": "text", "content": ... }. Added negative assertions ensuring no span attribute keys start with gen_ai.prompt. or gen_ai.completion.. Switched total-usage assertion to SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS. Removed unused MagicMock import.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nibbled at spans and stitched the message threads,
Packed prompts and answers into tidy JSON beds,
Images wrapped in base64, neatly tucked away,
One list of messages to carry what we say,
A rabbit's little hop—traces tidy for the day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: migrating span attributes to gen_ai.input/output.messages and fixing issue #3515.
Linked Issues check ✅ Passed The PR fully implements the objectives from #3515: replaces deprecated gen_ai.prompt/completion with gen_ai.input/output.messages in JSON-serialized message format with proper role and parts structure.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue objectives: migrating from deprecated attributes to structured message formats and fixing related bugs in span_utils.py and tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py (1)

49-57: Strengthen schema assertions for migrated message payloads.

Current checks only validate role; they won’t catch regressions in parts structure (type/content) that this migration depends on.

✅ Suggested test hardening
     assert GenAIAttributes.GEN_AI_INPUT_MESSAGES in attrs
     input_msgs = json.loads(attrs[GenAIAttributes.GEN_AI_INPUT_MESSAGES])
     assert len(input_msgs) > 0
     assert input_msgs[0]["role"] == "user"
+    assert isinstance(input_msgs[0].get("parts"), list) and len(input_msgs[0]["parts"]) > 0
+    assert input_msgs[0]["parts"][0]["type"] == "text"
+    assert "content" in input_msgs[0]["parts"][0]

     assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES in attrs
     output_msgs = json.loads(attrs[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES])
     assert len(output_msgs) > 0
     assert output_msgs[0]["role"] == "assistant"
+    assert isinstance(output_msgs[0].get("parts"), list) and len(output_msgs[0]["parts"]) > 0
+    assert output_msgs[0]["parts"][0]["type"] == "text"
+    assert "content" in output_msgs[0]["parts"][0]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`
around lines 49 - 57, The tests currently only assert the "role" field for
messages stored under GenAIAttributes.GEN_AI_INPUT_MESSAGES and
GenAIAttributes.GEN_AI_OUTPUT_MESSAGES; extend these assertions to validate the
migrated message payload schema by checking each message's "parts" structure
(e.g., ensure input_msgs[0] and output_msgs[0] have a "parts" list, that
parts[0] contains "type" and "content" keys, and that "content" is the expected
string or non-empty value), so the test fails if the required type/content
fields are missing or empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 409-433: The code currently hardcodes "finish_reason": "stop" for
every appended message; instead, only include a finish_reason field when the
non-streaming Google response provides one (use
response.candidates[0].finish_reason when present) and omit finish_reason for
streaming paths (where response was a list/str or built from response.text).
Update the branches that handle the original non-streaming response object (the
branches referencing response and response.text and appending into
output_messages) to read finish = getattr(response, "candidates", None) and, if
finish and finish[0].finish_reason is truthy, include "finish_reason":
finish[0].finish_reason in the dict; otherwise do not add the finish_reason key.
Leave the streaming branches that handle list/str (the paths enumerating
response or using response as str) unchanged so they do not include
finish_reason.

---

Nitpick comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`:
- Around line 49-57: The tests currently only assert the "role" field for
messages stored under GenAIAttributes.GEN_AI_INPUT_MESSAGES and
GenAIAttributes.GEN_AI_OUTPUT_MESSAGES; extend these assertions to validate the
migrated message payload schema by checking each message's "parts" structure
(e.g., ensure input_msgs[0] and output_msgs[0] have a "parts" list, that
parts[0] contains "type" and "content" keys, and that "content" is the expected
string or non-empty value), so the test fails if the required type/content
fields are missing or empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1215e26d-911c-4d0f-b60a-791811eb5b38

📥 Commits

Reviewing files that changed from the base of the PR and between 0a25803 and 7b328ac.

📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
  • packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py

…n unknown

Updated output message construction to include finish_reason when available and adjusted handling for both non-streaming and streaming responses.
… in messages

Add assertions to validate structure of input and output messages in tests.
@abhyudayareddy

Copy link
Copy Markdown
Author

Addressed both CodeRabbit review items in the latest two commits:

  1. finish_reason fix (4862ab3): replaced the hardcoded "finish_reason": "stop" with dynamic extraction from response.candidates[0].finish_reason for non-streaming responses (using .name.lower() on the enum), and omitted finish_reason entirely for streaming paths where it isn't reliably available per-chunk.

  2. Stronger test assertions (e6f86a8): added parts structure validation for both input and output messages — checks that parts is a non-empty list, that parts[0]["type"] == "text", and that "content" is present.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py (1)

49-63: Assert that the deprecated indexed attributes are gone.

These checks only prove the new JSON blobs exist. They would still pass if gen_ai.prompt.* / gen_ai.completion.* were emitted alongside them, which leaves the core migration behavior unguarded.

🧪 Minimal regression guard
     assert GenAIAttributes.GEN_AI_INPUT_MESSAGES in attrs
+    assert not any(key.startswith("gen_ai.prompt.") for key in attrs.keys())
     input_msgs = json.loads(attrs[GenAIAttributes.GEN_AI_INPUT_MESSAGES])

     assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES in attrs
+    assert not any(key.startswith("gen_ai.completion.") for key in attrs.keys())
     output_msgs = json.loads(attrs[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`
around lines 49 - 63, Add assertions to ensure deprecated indexed attributes are
not present: after loading attrs and verifying
GenAIAttributes.GEN_AI_INPUT_MESSAGES and
GenAIAttributes.GEN_AI_OUTPUT_MESSAGES, assert that no keys in attrs start with
"gen_ai.prompt." or "gen_ai.completion." (e.g., use
any(k.startswith("gen_ai.prompt.") or k.startswith("gen_ai.completion.") for k
in attrs) and assert that this is False). Reference the attrs variable and the
existing GenAIAttributes.GEN_AI_INPUT_MESSAGES /
GenAIAttributes.GEN_AI_OUTPUT_MESSAGES checks to locate where to add these
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 202-218: The current handling of kwargs["contents"] in this block
(and the similar block at 253-303) only handles str vs list and treats every
list item as a separate turn, which misclassifies single Content objects and
iterables of Part objects; change the normalization so that you first detect
whether contents is a Content-like object (has attributes role and parts) and
treat it as one turn, detect whether contents is a single Part (has attributes
type/content) and wrap it into a single turn with default role "user", and
detect whether contents is an iterable: if the iterable's items are Content
objects treat each as a separate turn, but if the iterable's items are Part
objects aggregate them into one turn. Use the existing _process_content_item to
process parts, preserve role via getattr(content_item, "role", "user"), and
append consistent dicts to input_messages with keys "role" and "parts"; apply
the same normalization logic in both the block around input_messages
construction and the similar code at lines 253-303.

---

Nitpick comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`:
- Around line 49-63: Add assertions to ensure deprecated indexed attributes are
not present: after loading attrs and verifying
GenAIAttributes.GEN_AI_INPUT_MESSAGES and
GenAIAttributes.GEN_AI_OUTPUT_MESSAGES, assert that no keys in attrs start with
"gen_ai.prompt." or "gen_ai.completion." (e.g., use
any(k.startswith("gen_ai.prompt.") or k.startswith("gen_ai.completion.") for k
in attrs) and assert that this is False). Reference the attrs variable and the
existing GenAIAttributes.GEN_AI_INPUT_MESSAGES /
GenAIAttributes.GEN_AI_OUTPUT_MESSAGES checks to locate where to add these
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 970a054d-55c6-4934-90f0-ee656e5f647a

📥 Commits

Reviewing files that changed from the base of the PR and between 7b328ac and e6f86a8.

📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
  • packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py

…bsent

Adds negative assertions to test_client_spans to verify deprecated gen_ai.prompt.{N} and gen_ai.completion.{N} attributes are absent after migrating to gen_ai.input.messages / gen_ai.output.messages. Addresses CodeRabbit review feedback (round 2).Adds negative assertions to test_client_spans to guard against deprecated gen_ai.prompt.{N} and gen_ai.completion.{N} attributes leaking back in after the migration to gen_ai.input.messages / gen_ai.output.messages. Addresses CodeRabbit review feedback.Added assertions to verify deprecated attributes are not emitted.
…tent] from list[Part] in contents normalization

Addresses CodeRabbit major review comment on contents normalization:

1. Adds elif hasattr(contents, "parts") branch to handle a single Content object passed directly (not wrapped in a list).

2. In the elif isinstance(contents, list) branch, sniffs contents[0] to distinguish multi-turn conversations (list[Content], each with .parts) from single-turn inputs (list[Part] or list[str]). Multi-turn: each Content becomes a separate message. Single-turn: all Parts are collected into one user message.

Applied symmetrically to both the async set_input_attributes and sync set_input_attributes_sync.
@abhyudayareddy

Copy link
Copy Markdown
Author

Addressed all CodeRabbit round-2 feedback (commits 08f2f74 and 56f5e51):

test_generate_content.py (commit 08f2f74): Added negative assertions after the existing gen_ai.input.messages / gen_ai.output.messages checks to guard against regression to the deprecated indexed attributes:

assert not any(key.startswith("gen_ai.prompt.") for key in attrs.keys())
assert not any(key.startswith("gen_ai.completion.") for key in attrs.keys())

span_utils.py (commit 56f5e51): Fixed contents normalization to correctly distinguish between input shapes:

  • str → single user message (unchanged)
  • Content object (has .parts): single turn, wrapped as one message
  • list[Content] (each item has .parts): multi-turn conversation, each item becomes a separate message with its own role
  • list[Part] or list[str]: single-turn, all items collected into one role: user message with multiple parts

The fix applies to both the async set_input_attributes and the sync set_input_attributes_sync paths.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)

123-138: ⚠️ Potential issue | 🟠 Major

Normalize Part inputs before serializing gen_ai.input.messages.

The SDK accepts contents as a single Part, groups non-function-call parts into a user turn, groups function-call parts into a model turn, and allows mixed ContentUnion lists to alternate between those grouped turns. This code still only special-cases str / Content / list, forces non-Content lists to "user", and falls back to str(part) for direct Part-like values, so valid tool-calling or multimodal inputs can be dropped or mis-serialized. (googleapis.github.io)

Also applies to: 202-239, 274-340

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 123 - 138, This block mishandles Part-like inputs by only
special-casing str/list and falling back to str(content_item), which can drop or
mis-serialize tool-calling or multimodal Parts; update the processing around
content_item to first normalize any Part/Part-like or ContentUnion instances
into canonical message parts (preserving function-call vs non-function-call
semantics and list alternation rules used by gen_ai.input.messages) before
serializing: detect Part objects (instead of treating them as generic objects),
route image Parts through _is_image_part/_process_image_part as before, group
non-function-call Parts into user turns and function-call Parts into model
turns, and handle single Part or single-list inputs identically to lists so you
never fall back to processed_content.append({"type":"text","content":
str(content_item)}); apply the same normalization logic to the other processing
sites referenced in the review (the blocks around lines handling content lists)
to ensure consistent serialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 445-459: The code currently reads a single finish_reason from
_candidates[0] and reuses it for every message in the multi-message branch;
change logic so each message uses its own candidate's finish_reason: iterate
over _candidates (or ensure the loop enumerates response/candidates) and for
each item use that item.finish_reason (handling hasattr(item.finish_reason,
"name") as done now) to set msg["finish_reason"] before appending to
output_messages; update references to response.text/list branching and the
variables _candidates, fr, and _finish_reason accordingly so each candidate's
finish_reason is applied to its own msg.
- Around line 443-487: The handler only serializes response.text; update the
logic in span_utils.py (the block building output_messages used before calling
_set_span_attribute with GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) to iterate
response.parts (and response.function_calls when present) in addition to
response.text so all part types (function_call, inline_data, file_data,
executable_code, code_execution_result, etc.) are captured into the parts list
structure, preserving existing fields like "type", "content" (or structured
payload for function calls), and adding finish_reason when available from
response.candidates; ensure GenerateContentResponse.parts entries are normalized
into the same message format currently used for text so downstream consumers
receive complete gen_ai.output.messages.

---

Duplicate comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 123-138: This block mishandles Part-like inputs by only
special-casing str/list and falling back to str(content_item), which can drop or
mis-serialize tool-calling or multimodal Parts; update the processing around
content_item to first normalize any Part/Part-like or ContentUnion instances
into canonical message parts (preserving function-call vs non-function-call
semantics and list alternation rules used by gen_ai.input.messages) before
serializing: detect Part objects (instead of treating them as generic objects),
route image Parts through _is_image_part/_process_image_part as before, group
non-function-call Parts into user turns and function-call Parts into model
turns, and handle single Part or single-list inputs identically to lists so you
never fall back to processed_content.append({"type":"text","content":
str(content_item)}); apply the same normalization logic to the other processing
sites referenced in the review (the blocks around lines handling content lists)
to ensure consistent serialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31c464ec-5fbf-4173-853e-e80ee36d116d

📥 Commits

Reviewing files that changed from the base of the PR and between e6f86a8 and 56f5e51.

📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
  • packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py
✅ Files skipped from review due to trivial changes (1)
  • packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py

@abhyudayareddy

Copy link
Copy Markdown
Author

Addressed CodeRabbit round-3 feedback in commit 6dc73dc:

Item 1 (Major — Handle all response.parts types): Added a _serialize_response_part(part) helper that serializes text parts, function_call parts (with name + args), and falls back to str(part) for other types. set_response_attributes now iterates over response.candidates, reads each candidate's content.parts, and serializes all parts through this helper — so tool-call and multimodal responses are captured in gen_ai.output.messages instead of being dropped.

Item 2 (Minor — Per-candidate finish_reason): Removed the single shared _finish_reason extracted from candidates[0]. Each candidate now reads its own finish_reason field inside the loop, so multi-candidate responses get the correct stop reason per message instead of all inheriting the first candidate's reason.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)

123-139: ⚠️ Potential issue | 🟠 Major

Structured inputs are still dropped or flattened outside the main contents path.

kwargs["contents"] still only matches str, objects with .parts, or list, so a single part-like object falls through without emitting anything. The list and positional-args branches also serialize .text / .parts objects via str(...) instead of the existing content/part serializers, which loses real text, roles, and turn boundaries in gen_ai.input.messages. Please funnel both kwargs and positional args through one shared normalizer.

Also applies to: 158-189, 202-248, 282-378

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 123 - 139, The code currently flattens or drops structured inputs
by only handling str, .parts lists, or list types and by using str(...) instead
of the proper serializers; update span_utils.py to funnel both
kwargs["contents"] and all positional args through a single shared normalizer
function (create a helper like _normalize_contents used by the blocks around
processed_content) that: detects single part-like objects (use _is_image_part)
and other content types, calls the existing serializers for content/part instead
of str(), and routes image parts to _process_image_part with
span.context.trace_id/span_id; replace the duplicated handling in the branches
referenced (including the shown block and the ranges 158-189, 202-248, 282-378)
to call this shared normalizer so gen_ai.input.messages preserves text, roles,
and turn boundaries.
🧹 Nitpick comments (1)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)

441-447: Align tool-call parts with the shared message schema.

This serializer emits {"type": "function_call"}, but packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/span_utils.py:57-94 already uses {"type": "tool_call"} for the same cross-provider part shape. Using a different type here makes downstream parsers provider-specific again.

♻️ Suggested change
         return {
-            "type": "function_call",
+            "type": "tool_call",
             "name": fc.name,
             "arguments": dict(fc.args) if hasattr(fc, "args") else {},
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 441 - 447, The serializer currently returns a dict with "type":
"function_call" for parts where part.function_call is present; change the
emitted type to "tool_call" and keep the same shape (keys "name" and
"arguments") so the output matches the cross-provider schema used by the other
implementation (see the handling of part.function_call -> fc.name and fc.args in
span_utils). Update the return inside the if hasattr(part, "function_call")
block to emit "type": "tool_call" and preserve converting fc.args to a dict when
present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 473-500: The candidate-level fallback that assigns response.text
when parts is empty should be moved out of the per-candidate loop so one
candidate cannot inherit another candidate’s text; instead, only if no candidate
produced any parts at all use the response-wide text to create a single
assistant message. Also fix the second fallback branch to check
isinstance(response.text, list) and iterate over response.text (not response)
when building messages. Update logic around parts, candidates iteration and the
output_messages append (references: parts, candidates loop, response.text,
output_messages, _finish_reason) accordingly.

---

Duplicate comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 123-139: The code currently flattens or drops structured inputs by
only handling str, .parts lists, or list types and by using str(...) instead of
the proper serializers; update span_utils.py to funnel both kwargs["contents"]
and all positional args through a single shared normalizer function (create a
helper like _normalize_contents used by the blocks around processed_content)
that: detects single part-like objects (use _is_image_part) and other content
types, calls the existing serializers for content/part instead of str(), and
routes image parts to _process_image_part with span.context.trace_id/span_id;
replace the duplicated handling in the branches referenced (including the shown
block and the ranges 158-189, 202-248, 282-378) to call this shared normalizer
so gen_ai.input.messages preserves text, roles, and turn boundaries.

---

Nitpick comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 441-447: The serializer currently returns a dict with "type":
"function_call" for parts where part.function_call is present; change the
emitted type to "tool_call" and keep the same shape (keys "name" and
"arguments") so the output matches the cross-provider schema used by the other
implementation (see the handling of part.function_call -> fc.name and fc.args in
span_utils). Update the return inside the if hasattr(part, "function_call")
block to emit "type": "tool_call" and preserve converting fc.args to a dict when
present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f62549c-6d10-4524-98d5-1d4a7bc278d8

📥 Commits

Reviewing files that changed from the base of the PR and between 56f5e51 and 6dc73dc.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py (1)

167-168: ⚠️ Potential issue | 🔴 Critical

Critical: MagicMock used but not imported.

The test uses MagicMock() on line 167, but there's no import for it. This will cause NameError: name 'MagicMock' is not defined when running this test.

🐛 Proposed fix - add import
 import json
 
 import pytest
+from unittest.mock import MagicMock
 from opentelemetry.trace import StatusCode, SpanKind
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`
around lines 167 - 168, The test uses MagicMock (see span = MagicMock() in
tests/test_generate_content.py) but MagicMock is not imported; add an import for
MagicMock (e.g., from unittest.mock import MagicMock) at the top of the test
module so the span = MagicMock() and span.is_recording.return_value = True lines
can run without NameError.
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (2)

720-735: ⚠️ Potential issue | 🔴 Critical

Critical: Variable um used but never assigned, causing NameError.

The code checks hasattr(response, "usage_metadata") but never assigns um. Lines 724, 729, and 734 reference um.total_token_count, um.candidates_token_count, and um.prompt_token_count which will fail at runtime.

🐛 Proposed fix
     _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, llm_model)
     if hasattr(response, "usage_metadata"):
+        um = response.usage_metadata
         _set_span_attribute(
             span,
             SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS,
             um.total_token_count,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 720 - 735, The code references an unassigned variable um when
reading usage fields; fix by reading the response.usage_metadata into a local
variable (e.g., um = response.usage_metadata) immediately after the
hasattr(response, "usage_metadata") check, guard for None, then call
_set_span_attribute using um.total_token_count, um.candidates_token_count, and
um.prompt_token_count; update the block around response, um, and
_set_span_attribute (and leave SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS /
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS /
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS unchanged) so it no longer raises
NameError.

740-741: ⚠️ Potential issue | 🔴 Critical

Add missing imports for _GCP_GEN_AI and _GEN_CONTENT constants.

These constants are used at lines 740-741 in span_utils.py but are not imported. They are defined in __init__.py and should be imported from there to avoid NameError at runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 740 - 741, Import the missing constants _GCP_GEN_AI and
_GEN_CONTENT into span_utils.py so they are available where used in the
attributes dict (GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT); add them to the
module-level imports by importing _GCP_GEN_AI and _GEN_CONTENT from the package
root (where they are defined in __init__.py) alongside the other constants to
prevent NameError at runtime.
♻️ Duplicate comments (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (2)

663-670: ⚠️ Potential issue | 🟡 Minor

Response-wide response.text fallback inside per-candidate loop may cause incorrect attribution.

The fallback to response.text (lines 665-668) is inside the per-candidate loop. For multi-candidate responses, if any candidate has empty content.parts, it will use the response-wide response.text which typically reflects only the first candidate's text. Consider moving this fallback outside the loop or limiting it to single-candidate responses.

🛡️ Suggested guard
                 if not parts:
-                    # Fallback: try response.text for simple single-part responses
-                    try:
-                        text = response.text
-                        if text:
-                            parts = [{"type": "text", "content": text}]
-                    except Exception:
-                        pass
+                    # Fallback only for single-candidate responses
+                    if len(_candidates) == 1:
+                        try:
+                            text = response.text
+                            if text:
+                                parts = [{"type": "text", "content": text}]
+                        except Exception:
+                            pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 663 - 670, The per-candidate fallback that sets parts from
response.text should not run inside the per-candidate loop because response.text
typically reflects the first candidate and will misattribute text to other
candidates; update the logic in span_utils.py so that you only use response.text
when the response has a single candidate (e.g., check len(response.candidates)
== 1) or move the response.text fallback out of the candidate-processing loop
and apply it only when no candidates produced any parts; adjust the branch that
currently checks "if not parts:" inside the candidate loop to instead use a
guarded check (single-candidate) or perform a post-loop fallback using
response.text.

676-691: ⚠️ Potential issue | 🟠 Major

Fallback branch iterates response instead of response.text.

When response.text is a list (line 679), the code iterates over response (line 680) rather than response.text. Additionally, accessing item.text assumes items are objects with a text attribute, which may not be true if response.text contains strings.

🐛 Proposed fix
         else:
             # No candidates field: fall back to response.text
             try:
                 if isinstance(response.text, list):
-                    for item in response:
+                    for item in response.text:
                         output_messages.append({
                             "role": "assistant",
-                            "parts": [{"type": "text", "content": item.text}],
+                            "parts": [{"type": "text", "content": getattr(item, "text", str(item))}],
                         })
                 elif isinstance(response.text, str):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 676 - 691, In the else fallback in span_utils.py (the block
handling response.text when no candidates field exists), fix the loop to iterate
over response.text (not response) and handle list item types safely: for each
item in response.text, if the item is a string use it directly as the "content",
otherwise if it has a .text attribute use that, and skip or stringify other
types; update the branch inside the function that builds output_messages so it
checks isinstance(response.text, list) and accesses items correctly instead of
using item.text blindly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 238-261: The function _process_content_item uses an undefined
accumulator and helper: replace the erroneous parts_acc usage with the local
processed_content (i.e., call processed_content.extend(...)) and ensure the
helper _parts_from_genai_part_async is provided — either implement/import a
coroutine named _parts_from_genai_part_async(part, span, part_index) that
returns a list of processed parts or rename the call to the correct existing
async helper if it already exists; update _process_content_item to extend
processed_content with the awaited result and return processed_content.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`:
- Around line 76-78: The test asserts SpanAttributes.LLM_USAGE_TOTAL_TOKENS
while the implementation sets SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, causing
a mismatch; fix by making them consistent: either update the test to assert
SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS (and related GEN_AI_* attributes) or
update the implementation (the code that assigns token attributes in
span_utils.py) to also set the LLM_USAGE_* attributes (or set both keys) so both
constants are present; ensure you update or add the corresponding assertions for
input/output/total tokens (SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS) consistently.

---

Outside diff comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 720-735: The code references an unassigned variable um when
reading usage fields; fix by reading the response.usage_metadata into a local
variable (e.g., um = response.usage_metadata) immediately after the
hasattr(response, "usage_metadata") check, guard for None, then call
_set_span_attribute using um.total_token_count, um.candidates_token_count, and
um.prompt_token_count; update the block around response, um, and
_set_span_attribute (and leave SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS /
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS /
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS unchanged) so it no longer raises
NameError.
- Around line 740-741: Import the missing constants _GCP_GEN_AI and _GEN_CONTENT
into span_utils.py so they are available where used in the attributes dict
(GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT); add them to the
module-level imports by importing _GCP_GEN_AI and _GEN_CONTENT from the package
root (where they are defined in __init__.py) alongside the other constants to
prevent NameError at runtime.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`:
- Around line 167-168: The test uses MagicMock (see span = MagicMock() in
tests/test_generate_content.py) but MagicMock is not imported; add an import for
MagicMock (e.g., from unittest.mock import MagicMock) at the top of the test
module so the span = MagicMock() and span.is_recording.return_value = True lines
can run without NameError.

---

Duplicate comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 663-670: The per-candidate fallback that sets parts from
response.text should not run inside the per-candidate loop because response.text
typically reflects the first candidate and will misattribute text to other
candidates; update the logic in span_utils.py so that you only use response.text
when the response has a single candidate (e.g., check len(response.candidates)
== 1) or move the response.text fallback out of the candidate-processing loop
and apply it only when no candidates produced any parts; adjust the branch that
currently checks "if not parts:" inside the candidate loop to instead use a
guarded check (single-candidate) or perform a post-loop fallback using
response.text.
- Around line 676-691: In the else fallback in span_utils.py (the block handling
response.text when no candidates field exists), fix the loop to iterate over
response.text (not response) and handle list item types safely: for each item in
response.text, if the item is a string use it directly as the "content",
otherwise if it has a .text attribute use that, and skip or stringify other
types; update the branch inside the function that builds output_messages so it
checks isinstance(response.text, list) and accesses items correctly instead of
using item.text blindly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eadafac1-a58b-43fc-9f8c-5c6cfeb6ada0

📥 Commits

Reviewing files that changed from the base of the PR and between 6dc73dc and e1f2f5c.

📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
  • packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py

@abhyudayareddy

Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
✅ Actions performed

Reviews resumed.

@abhyudayareddy

Copy link
Copy Markdown
Author

👋 Hi @nirga, @galkleinman, @dinmukhamedm — could you take a look at this PR when you get a chance? It migrates the Google GenAI instrumentation span attributes to the stable gen_ai.input/output.messages convention (fixes #3515). CodeRabbit review has been resumed and all conflicts are resolved. Thanks!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py (1)

1-12: ⚠️ Potential issue | 🔴 Critical

Missing MagicMock import causes NameError in test.

Line 167 uses MagicMock() but the import was removed. This will cause a NameError when running test_set_model_request_attributes_reads_system_instruction_from_config.

🐛 Proposed fix
 import json
 
 import pytest
+from unittest.mock import MagicMock
 from opentelemetry.trace import StatusCode, SpanKind
 from opentelemetry.semconv_ai import (
     SpanAttributes,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`
around lines 1 - 12, The test fails because MagicMock is used but not imported;
add "from unittest.mock import MagicMock" to the imports at the top of the test
module so tests like
test_set_model_request_attributes_reads_system_instruction_from_config can
instantiate MagicMock() without a NameError.
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)

720-735: ⚠️ Potential issue | 🔴 Critical

Critical: um is referenced but never assigned, causing NameError at runtime.

Lines 724, 729, 734, and 738 use um.total_token_count, um.candidates_token_count, and um.prompt_token_count, but the variable um is never assigned from response.usage_metadata.

🐛 Proposed fix
     _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, llm_model)
     if hasattr(response, "usage_metadata"):
+        um = response.usage_metadata
         _set_span_attribute(
             span,
             SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS,
             um.total_token_count,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 720 - 735, The code block in span_utils.py references
um.total_token_count / um.candidates_token_count / um.prompt_token_count but
never assigns um from response.usage_metadata; set um = response.usage_metadata
(after confirming hasattr(response, "usage_metadata") and that it is not None)
before calling _set_span_attribute, and keep the existing guards around
_set_span_attribute for SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS and the
GenAIAttributes to avoid NameError and handle missing usage metadata.
♻️ Duplicate comments (1)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)

238-261: ⚠️ Potential issue | 🔴 Critical

Critical: Undefined variable parts_acc and function _parts_from_genai_part_async cause runtime NameError.

Line 244 references parts_acc but the function initializes processed_content. Additionally, _parts_from_genai_part_async is never defined—the correct helper is _process_content_part (lines 264-277).

🐛 Proposed fix
 async def _process_content_item(content_item, span):
     """Process a single content item, handling different types (Content objects, strings, Parts)"""
     processed_content = []
     if hasattr(content_item, "parts"):
         # Content with parts (Google GenAI Content object)
         for part_index, part in enumerate(content_item.parts):
-            parts_acc.extend(await _parts_from_genai_part_async(part, span, part_index))
+            processed_part = await _process_content_part(part, span, part_index)
+            if processed_part:
+                processed_content.append(processed_part)
     elif isinstance(content_item, str):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 238 - 261, The function _process_content_item currently references
an undefined parts_acc and calls a non-existent helper
_parts_from_genai_part_async; update the loop that handles content_item.parts to
call the existing _process_content_part helper, await it, and extend the local
processed_content list with its returned list (e.g. for part_index, part in
enumerate(content_item.parts): processed_content.extend(await
_process_content_part(part, span, part_index))). Remove references to parts_acc
and ensure the signature/arguments match _process_content_part (part, span,
part_index) so the function returns a combined processed_content list for all
branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 676-691: The fallback branch that handles non-candidates responses
checks isinstance(response.text, list) but incorrectly iterates "for item in
response" instead of the list; update the loop in the span_utils fallback to
iterate over response.text (e.g., "for item in response.text") and then use item
or item.text consistently when building output_messages so output_messages is
populated from the actual text list rather than the response object.
- Around line 747-754: Import the missing constants and use them instead of
hardcoded strings: add an import for _GCP_GEN_AI (and _GEN_CONTENT if used) from
opentelemetry.instrumentation.google_generativeai at the top of span_utils.py,
then replace the hardcoded "Google" and any literal token-type strings in the
token_histogram.record call (the call in span_utils.py that references
token_histogram.record, GenAIAttributes.GEN_AI_PROVIDER_NAME and
GenAIAttributes.GEN_AI_TOKEN_TYPE) with the appropriate constants (_GCP_GEN_AI
and _GEN_CONTENT) so the names exist at runtime and remain consistent.

---

Outside diff comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 720-735: The code block in span_utils.py references
um.total_token_count / um.candidates_token_count / um.prompt_token_count but
never assigns um from response.usage_metadata; set um = response.usage_metadata
(after confirming hasattr(response, "usage_metadata") and that it is not None)
before calling _set_span_attribute, and keep the existing guards around
_set_span_attribute for SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS and the
GenAIAttributes to avoid NameError and handle missing usage metadata.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`:
- Around line 1-12: The test fails because MagicMock is used but not imported;
add "from unittest.mock import MagicMock" to the imports at the top of the test
module so tests like
test_set_model_request_attributes_reads_system_instruction_from_config can
instantiate MagicMock() without a NameError.

---

Duplicate comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 238-261: The function _process_content_item currently references
an undefined parts_acc and calls a non-existent helper
_parts_from_genai_part_async; update the loop that handles content_item.parts to
call the existing _process_content_part helper, await it, and extend the local
processed_content list with its returned list (e.g. for part_index, part in
enumerate(content_item.parts): processed_content.extend(await
_process_content_part(part, span, part_index))). Remove references to parts_acc
and ensure the signature/arguments match _process_content_part (part, span,
part_index) so the function returns a combined processed_content list for all
branches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 976d58b1-1da2-4744-97de-ca2deedb13be

📥 Commits

Reviewing files that changed from the base of the PR and between 6dc73dc and e1f2f5c.

📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
  • packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py

@abhyudayareddy

Copy link
Copy Markdown
Author

Fixed the Major issue flagged by CodeRabbit. The _finish_reason extraction in the non-streaming output path was using fr.name.lower() directly, which produces raw Gemini enum names like "max_tokens" and "safety" instead of the OTel GenAI semantic convention values ("length" and "content_filter" respectively).

The fix routes through the existing _map_gemini_finish_reason() helper that already has the correct STOP→stop, MAX_TOKENS→length, SAFETY/RECITATION/BLOCKLIST→content_filter, MALFORMED_FUNCTION_CALL→error mappings.

Commit: fix(google-genai): use _map_gemini_finish_reason() for OTel-compliant finish_reason values

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)

592-601: ⚠️ Potential issue | 🔴 Critical

Critical: Undefined function _parts_from_genai_part_sync will cause runtime error.

Line 599 calls _parts_from_genai_part_sync which was removed during this refactoring but the call site wasn't updated. This will raise NameError when processing system instructions that contain parts.

Consider implementing inline part processing similar to the sync input handling:

 def _system_instruction_to_parts(si, span):
     """OTel: flat array of parts for gen_ai.system_instructions."""
     if isinstance(si, str):
         return [{"type": "text", "content": si}]
     if hasattr(si, "parts") and si.parts:
         out = []
         for idx, p in enumerate(si.parts):
-            out.extend(_parts_from_genai_part_sync(p, span, idx))
+            if hasattr(p, "text") and p.text:
+                out.append({"type": "text", "content": p.text})
+            elif _is_image_part(p):
+                img = _process_image_part_sync(p, span.context.trace_id, span.context.span_id, idx)
+                if img:
+                    out.append(img)
+            else:
+                out.append({"type": "text", "content": str(p)})
         return out
     return [{"type": "text", "content": str(si)}]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 592 - 601, _system_instruction_to_parts currently calls a removed
helper `_parts_from_genai_part_sync`, causing a NameError; replace that call
with the existing part-processing logic used elsewhere by either calling the
correct helper (e.g., `_parts_from_genai_part`) or inlining the same logic used
for sync input handling: iterate si.parts, enumerate to get idx, and for each
part p call the correct part-to-span conversion routine (passing span and idx)
to extend out; ensure you preserve the returned structure (list of dicts) and
the fallback string conversion path.
♻️ Duplicate comments (3)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (3)

238-261: ⚠️ Potential issue | 🔴 Critical

Critical: Undefined variable parts_acc and function _parts_from_genai_part_async will cause runtime errors.

Line 244 references parts_acc but the function initializes processed_content on line 240. Additionally, _parts_from_genai_part_async is never defined in this module. This will raise NameError when processing Content objects with parts.

The fix should use the existing _process_content_part helper:

 async def _process_content_item(content_item, span):
     """Process a single content item, handling different types (Content objects, strings, Parts)"""
     processed_content = []
     if hasattr(content_item, "parts"):
         # Content with parts (Google GenAI Content object)
         for part_index, part in enumerate(content_item.parts):
-            parts_acc.extend(await _parts_from_genai_part_async(part, span, part_index))
+            processed_part = await _process_content_part(part, span, part_index)
+            if processed_part:
+                processed_content.append(processed_part)
     elif isinstance(content_item, str):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 238 - 261, In _process_content_item the code wrongly references an
undefined variable parts_acc and calls a non-existent helper
_parts_from_genai_part_async; replace that branch to iterate content_item.parts
and call the existing _process_content_part for each part (passing span,
part_index and/or trace/span ids as needed) and extend processed_content with
its results, keeping the existing image handling via _process_image_part and
returning processed_content; update references to span.context.trace_id and
span.context.span_id only where _process_content_part requires them.

720-754: ⚠️ Potential issue | 🔴 Critical

Critical: Undefined variable um and missing constant imports will cause runtime errors.

Multiple undefined names in this function:

  1. Lines 723-734: Uses um.total_token_count, um.candidates_token_count, um.prompt_token_count but um is never assigned. Should be response.usage_metadata or assign um = response.usage_metadata first.

  2. Lines 740-741: Uses _GCP_GEN_AI and _GEN_CONTENT constants that are not imported in this module.

  3. Line 750: Uses hardcoded "Google" while line 740 expects the constant _GCP_GEN_AI (inconsistent).

 def set_model_response_attributes(
     span, response, llm_model, token_histogram, stream_finish_reasons=None
 ):
     if not span.is_recording():
         return
     _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, llm_model)
     if hasattr(response, "usage_metadata"):
+        um = response.usage_metadata
         _set_span_attribute(
             span,
             SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS,
             um.total_token_count,
         )

Additionally, add the missing imports at the top of the file or use inline values consistently:

from opentelemetry.instrumentation.google_generativeai import _GCP_GEN_AI, _GEN_CONTENT

Or replace with string literals consistently throughout (e.g., "Google" and "generate_content").


663-690: ⚠️ Potential issue | 🟠 Major

Move the fallback out of the candidate loop and fix the iteration target.

Two issues in the fallback handling:

  1. Lines 663-670: The response.text fallback is inside the per-candidate loop. If any candidate has empty content.parts, it will use response-wide text, potentially causing one candidate to inherit another's content.

  2. Lines 679-684: The code checks isinstance(response.text, list) but iterates for item in response: instead of for item in response.text:.

                 if parts:
                     msg = {"role": "assistant", "parts": parts}
                     if _finish_reason:
                         msg["finish_reason"] = _finish_reason
                     output_messages.append(msg)
+        # Global fallback: only if no candidates produced parts
+        if not output_messages:
+            try:
+                text = response.text
+                if text:
+                    output_messages.append({
+                        "role": "assistant",
+                        "parts": [{"type": "text", "content": text}],
+                    })
+            except Exception:
+                pass
         else:
             # No candidates field: fall back to response.text
             try:
                 if isinstance(response.text, list):
-                    for item in response:
+                    for item in response.text:
                         output_messages.append({
                             "role": "assistant",
-                            "parts": [{"type": "text", "content": item.text}],
+                            "parts": [{"type": "text", "content": item if isinstance(item, str) else getattr(item, "text", str(item))}],
                         })

And remove the per-candidate fallback (lines 663-670).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 663 - 690, The per-candidate fallback that reads response.text
(currently inside the candidate-processing loop in span_utils.py) must be
removed so a candidate with empty content.parts doesn't pull in response-wide
text; instead, after finishing the candidates loop, add a single response-level
fallback that inspects response.text once and appends one assistant message if
appropriate. Also fix the list-iteration bug: when handling the response.text
fallback, check isinstance(response.text, list) and iterate for item in
response.text (not for item in response), and when items are strings or objects
use their appropriate .text/value to build parts; remove the per-candidate
fallback block and implement the corrected post-loop response.text handling in
the same function where parts, msg, and output_messages are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 14-16: Fix the broken import in span_utils.py: the import of
SpanAttributes from opentelemetry.semconv_ai has a missing opening parenthesis
and line-breaked syntax causing a SyntaxError. Replace the malformed lines with
a valid import statement for SpanAttributes (e.g., import SpanAttributes
correctly from opentelemetry.semconv_ai) so the module can load; locate the
faulty import near the top of span_utils.py where SpanAttributes is referenced.

---

Outside diff comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 592-601: _system_instruction_to_parts currently calls a removed
helper `_parts_from_genai_part_sync`, causing a NameError; replace that call
with the existing part-processing logic used elsewhere by either calling the
correct helper (e.g., `_parts_from_genai_part`) or inlining the same logic used
for sync input handling: iterate si.parts, enumerate to get idx, and for each
part p call the correct part-to-span conversion routine (passing span and idx)
to extend out; ensure you preserve the returned structure (list of dicts) and
the fallback string conversion path.

---

Duplicate comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 238-261: In _process_content_item the code wrongly references an
undefined variable parts_acc and calls a non-existent helper
_parts_from_genai_part_async; replace that branch to iterate content_item.parts
and call the existing _process_content_part for each part (passing span,
part_index and/or trace/span ids as needed) and extend processed_content with
its results, keeping the existing image handling via _process_image_part and
returning processed_content; update references to span.context.trace_id and
span.context.span_id only where _process_content_part requires them.
- Around line 663-690: The per-candidate fallback that reads response.text
(currently inside the candidate-processing loop in span_utils.py) must be
removed so a candidate with empty content.parts doesn't pull in response-wide
text; instead, after finishing the candidates loop, add a single response-level
fallback that inspects response.text once and appends one assistant message if
appropriate. Also fix the list-iteration bug: when handling the response.text
fallback, check isinstance(response.text, list) and iterate for item in
response.text (not for item in response), and when items are strings or objects
use their appropriate .text/value to build parts; remove the per-candidate
fallback block and implement the corrected post-loop response.text handling in
the same function where parts, msg, and output_messages are used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40aae1cd-c3ae-43da-bf8f-5721dae06482

📥 Commits

Reviewing files that changed from the base of the PR and between e1f2f5c and 78acebc.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py

…genai_part_async in _process_content_item

Refactor content processing to use _process_content_part for better handling of parts.
@abhyudayareddy

Copy link
Copy Markdown
Author

Addressed the two Critical issues flagged by CodeRabbit:

Fix 1 — span_utils.py line 244: NameError + undefined function

The original line called parts_acc.extend(await _parts_from_genai_part_async(...)) which had two problems:

  • parts_acc is not defined in _process_content_item — the list is called processed_content
    • _parts_from_genai_part_async is never defined anywhere in the module
      Replaced with the correct pattern using the existing _process_content_part function (which returns a single dict, not a list):
result = await _process_content_part(part, span, part_index)
if result:
    processed_content.append(result)

Fix 2 — test_generate_content.py line 76: legacy token attribute

The test was asserting against SpanAttributes.LLM_USAGE_TOTAL_TOKENS (deprecated) while the implementation sets SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS. Aligned the test to match the implementation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (2)

594-603: ⚠️ Potential issue | 🔴 Critical

Critical: _parts_from_genai_part_sync is undefined and will cause NameError.

Line 601 calls _parts_from_genai_part_sync which does not exist in this module. This will crash at runtime when processing system instructions that contain parts.

🐛 Proposed fix using existing helpers
 def _system_instruction_to_parts(si, span):
     """OTel: flat array of parts for gen_ai.system_instructions."""
     if isinstance(si, str):
         return [{"type": "text", "content": si}]
     if hasattr(si, "parts") and si.parts:
         out = []
         for idx, p in enumerate(si.parts):
-            out.extend(_parts_from_genai_part_sync(p, span, idx))
+            if hasattr(p, "text") and p.text:
+                out.append({"type": "text", "content": p.text})
+            elif _is_image_part(p):
+                img = _otel_image_part_from_genai_part(p, span, idx, sync=True)
+                if img:
+                    out.append(img)
+            else:
+                out.append({"type": "text", "content": str(p)})
         return out
     return [{"type": "text", "content": str(si)}]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 594 - 603, The function _system_instruction_to_parts calls an
undefined helper _parts_from_genai_part_sync causing a NameError; update the
call to use the existing helper (e.g., _parts_from_genai_part) or add a thin
synchronous wrapper that delegates to the existing implementation so that
_system_instruction_to_parts correctly iterates si.parts without referencing a
nonexistent symbol; change the call site in _system_instruction_to_parts (the
enumerate loop) to invoke the actual helper name used elsewhere in this module.

722-747: ⚠️ Potential issue | 🔴 Critical

Critical: Variable um is used but never defined.

Lines 722-747 reference um.total_token_count, um.candidates_token_count, and um.prompt_token_count without defining um, causing NameError for every non-streaming response. Additionally, _GCP_GEN_AI and _GEN_CONTENT used in lines 742-743 are not imported in this module.

🐛 Proposed fix

Add to imports at the top of the file:

from opentelemetry.instrumentation.google_generativeai import (
    _GCP_GEN_AI,
    _GEN_CONTENT,
)

Then fix the function by assigning um before use:

 def set_model_response_attributes(
     span, response, llm_model, token_histogram, stream_finish_reasons=None
 ):
     if not span.is_recording():
         return
     _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, llm_model)
     if hasattr(response, "usage_metadata"):
+        um = response.usage_metadata
         _set_span_attribute(
             span,
             SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS,
             um.total_token_count,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 722 - 747, The code uses an undefined variable um and missing
constants; fix by first assigning um = response.usage_metadata after the
hasattr(response, "usage_metadata") check (so the spans and token_histogram use
the actual usage_metadata object) and import the missing constants (_GCP_GEN_AI
and _GEN_CONTENT) at the top of the module from
opentelemetry.instrumentation.google_generativeai; update references in
span_utils.py (the block that sets GEN_AI_USAGE_* attributes and the
token_histogram.record call) to use the newly assigned um and the imported
constants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 338-350: When building input_messages, the role value must be
normalized using _normalize_message_role so Google "model" becomes the OTel
"assistant"; update each place where role is set (e.g., when handling a single
Content object and in the list branches using variables content_item and
contents and the _process_content_item call) to call
_normalize_message_role(getattr(..., "role", "user")) before appending to
input_messages; apply this change in both the async path (where role is assigned
from content_item or contents) and the sync path locations referenced (the
places currently using getattr(..., "role", "user") such as the spots around
input_messages construction and where role is assigned) so tests like
test_async_input.py pass.

---

Outside diff comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 594-603: The function _system_instruction_to_parts calls an
undefined helper _parts_from_genai_part_sync causing a NameError; update the
call to use the existing helper (e.g., _parts_from_genai_part) or add a thin
synchronous wrapper that delegates to the existing implementation so that
_system_instruction_to_parts correctly iterates si.parts without referencing a
nonexistent symbol; change the call site in _system_instruction_to_parts (the
enumerate loop) to invoke the actual helper name used elsewhere in this module.
- Around line 722-747: The code uses an undefined variable um and missing
constants; fix by first assigning um = response.usage_metadata after the
hasattr(response, "usage_metadata") check (so the spans and token_histogram use
the actual usage_metadata object) and import the missing constants (_GCP_GEN_AI
and _GEN_CONTENT) at the top of the module from
opentelemetry.instrumentation.google_generativeai; update references in
span_utils.py (the block that sets GEN_AI_USAGE_* attributes and the
token_histogram.record call) to use the newly assigned um and the imported
constants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7591ad68-4233-45ad-ba12-06feae28c127

📥 Commits

Reviewing files that changed from the base of the PR and between 78acebc and d0053a9.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py (1)

162-168: ⚠️ Potential issue | 🔴 Critical

Add missing import for MagicMock to fix test execution.

Line 167 uses MagicMock() but the class is not imported, causing a NameError at runtime.

Fix
 import pytest
+from unittest.mock import MagicMock
 from opentelemetry.trace import StatusCode, SpanKind
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`
around lines 162 - 168, The test
test_set_model_request_attributes_reads_system_instruction_from_config
references MagicMock but doesn't import it; add the missing import (import
MagicMock from unittest.mock) at the top of the test file so MagicMock used in
the span setup (span = MagicMock()) is defined and the test can run.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py`:
- Around line 162-168: The test
test_set_model_request_attributes_reads_system_instruction_from_config
references MagicMock but doesn't import it; add the missing import (import
MagicMock from unittest.mock) at the top of the test file so MagicMock used in
the span setup (span = MagicMock()) is defined and the test can run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 11fb6d37-f023-41f5-a362-680f6761b0b8

📥 Commits

Reviewing files that changed from the base of the PR and between d0053a9 and 4405529.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-google-generativeai/tests/test_generate_content.py

@abhyudayareddy

Copy link
Copy Markdown
Author

Hi team — just flagging that the failing security/snyk check appears to be a repository quota/infrastructure issue ("You have used your limit of pr...") rather than a code problem in this PR. The other CI checks are passing. Would appreciate a human review when you get a chance. Thanks!

@abhyudayareddy

Copy link
Copy Markdown
Author

Addressing all CodeRabbit inline review suggestions below. No human maintainer comments exist yet. Snyk failure on Apr 19 was a quota/rate-limit issue (not a vulnerability in this PR's code) — confirmed by CodeRabbit's pre-merge check status showing 4 pass / 1 fail which matches the known Snyk quota problem.


CodeRabbit suggestion assessment

CR-1 (Major): response.text fallback inside candidate loop can bleed across candidates

Valid. This is a real semantic bug.

When candidate.content is missing or content.parts is empty, the current fallback reaches for response.text — but that is a response-level property, not per-candidate. In a multi-candidate response, any candidate with empty parts will inherit whatever text happens to be in response.text, which could be text from a completely different candidate. This silently produces wrong span data for multi-candidate calls.

Fix: Guard the fallback with if idx == 0: or, better, scope it outside the candidate loop so it only fires when no candidates produced output at all. Do not use response.text as a per-candidate fallback.


CR-2 (Major): for item in response: should be for item in response.text:

Valid. This is a definite bug.

Line 679 correctly checks isinstance(response.text, list), but line 680 then iterates for item in response: — iterating the GenerateContentResponse object itself, not the list response.text. The Google GenAI SDK's response object may not be iterable in this way, and even if it is, iterating it does not yield text items. The fix is a one-character change: for item in response.text:.


CR-3 (Critical): _GCP_GEN_AI and _GEN_CONTENT used at line 750 but not imported in span_utils.py

Valid. This would cause a NameError at runtime.

These constants are defined in __init__.py (the instrumentation entrypoint) but span_utils.py does not import them. The proposed fix in CodeRabbit's earlier review rounds was incomplete because it referenced the constants without adding the import. Additionally, line 750 uses the hardcoded string "Google" while line 740 uses the constant _GCP_GEN_AI — the two should be consistent.

Fix: Either import _GCP_GEN_AI and _GEN_CONTENT from the appropriate module into span_utils.py, or inline the string literals consistently (and remove the dead constant reference). Do not mix constant references with hardcoded strings for the same logical value.


CR-4 (Critical): Missing opening parenthesis on import at line 14

Valid. This is a SyntaxError that prevents the module from loading at all.

# Current (broken):
from opentelemetry.semconv_ai import 
    SpanAttributes,

# Fixed:
from opentelemetry.semconv_ai import (
    SpanAttributes,
)

This would cause an immediate SyntaxError on import, meaning none of the instrumentation works. This must be fixed before any other issue matters.


Summary of required actions

# Severity Issue Action
CR-4 Critical Missing ( in import on line 14 Fix syntax error
CR-3 Critical _GCP_GEN_AI/_GEN_CONTENT used but not imported Add import or inline literal consistently
CR-1 Major response.text fallback bleeds across candidates in loop Scope fallback outside candidate loop
CR-2 Major for item in response: should be for item in response.text: Fix iteration target

CR-4 and CR-3 are blockers — the module cannot load with a syntax error, and the token histogram code will throw NameError at runtime if those constants are not in scope. CR-1 and CR-2 affect correctness of span data for streaming/multi-candidate responses.

The core gen_ai.input.messages / gen_ai.output.messages migration (the primary purpose of this PR) is correct: using a JSON-serialized list of {"role": "...", "parts": [...]} objects aligns with the OTel GenAI semantic conventions spec and is consistent with the OpenAI instrumentation in this repo. The attribute naming itself does not need changes.

@abhyudayareddy

Copy link
Copy Markdown
Author

Code Review: 4 Critical Bugs in span_utils.py

Reviewing the diff in packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py — found four bugs that will cause runtime failures.


CR-4 (SyntaxError) — Missing ( in multi-line import, line 14

File: span_utils.py
Line: 14

# BEFORE (broken — SyntaxError: invalid syntax)
from opentelemetry.semconv_ai import 
    SpanAttributes,
)

# AFTER (fixed)
from opentelemetry.semconv_ai import (
    SpanAttributes,
)

The opening parenthesis for the multi-line import was dropped. This is a hard SyntaxError — the module cannot be imported at all.


CR-3 (NameError) — _GCP_GEN_AI / _GEN_CONTENT used but never defined; "Google" hardcoded inconsistently

File: span_utils.py
Lines: 19–20 (missing module-level constants), 742–743, 752

The two module-level constants that existed in main were dropped in this PR:

# MISSING — add back after the imports (before the logger line)
_GCP_GEN_AI = GenAIAttributes.GenAiProviderNameValues.GCP_GEN_AI.value
_GEN_CONTENT = GenAIAttributes.GenAiOperationNameValues.GENERATE_CONTENT.value

Lines 742–743 reference these undeclared names → instant NameError at runtime when token_histogram is truthy.

Line 752 hardcodes the string "Google" instead of using the constant — inconsistent with lines 742–743:

# BEFORE (line 752 — inconsistent hardcoded string)
                GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google",

# AFTER (use the same constant)
                GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,

CR-2 (wrong iteration) — for item in response: should be for item in response.text:

File: span_utils.py
Line: 682

# BEFORE (iterates over the response object itself — wrong)
                    for item in response:

# AFTER (iterates over the list returned by response.text)
                    for item in response.text:

The guard on line 681 already checks isinstance(response.text, list), so the loop must iterate over response.text, not the raw response object. Iterating over response will either raise a TypeError or silently produce wrong data.


CR-1 (candidate bleed) — response.text fallback is inside the for candidate loop

File: span_utils.py
Lines: 665–672

The response.text fallback sits inside for candidate in _candidates: (lines 655–677). When a candidate has no content.parts, it appends response.text once per candidate — duplicating the same output message N times.

# BEFORE — fallback inside the loop (causes N-candidate bleed)
            for candidate in _candidates:
                ...
                if not parts:
                    try:
                        text = response.text
                        if text:
                            parts = [{"type": "text", "content": text}]
                    except Exception:
                        pass
                if parts:
                    output_messages.append(...)

# AFTER — remove the fallback from inside the loop; add it once after the loop
            for candidate in _candidates:
                ...
                # (remove the response.text fallback block here)
                if parts:
                    output_messages.append(...)

            # Fallback: only fires if no candidate yielded any parts at all
            if not output_messages:
                try:
                    text = response.text
                    if text:
                        output_messages.append({
                            "role": "assistant",
                            "parts": [{"type": "text", "content": text}],
                        })
                except Exception:
                    pass

Summary:

ID Line(s) Issue Severity
CR-4 14 Missing ( in from opentelemetry.semconv_ai import Critical — SyntaxError, module unimportable
CR-3 19–20 (missing), 752 _GCP_GEN_AI/_GEN_CONTENT not defined; "Google" hardcoded Critical — NameError at runtime
CR-2 682 for item in response: instead of for item in response.text: High — TypeError or wrong output
CR-1 665–672 response.text fallback inside candidate loop Medium — duplicated output messages

@abhyudayareddy

Copy link
Copy Markdown
Author

Fixed all 4 issues identified in code review:

  • CR-4: Added missing ( in multi-line import (was causing SyntaxError on module load)
  • CR-3: Added _GCP_GEN_AI and _GEN_CONTENT module-level constants; replaced hardcoded "Google" with _GCP_GEN_AI for consistency
  • CR-2: Fixed loop variable — for item in response:for item in response.text:
  • CR-1: Moved response.text fallback outside the for candidate in _candidates: loop to prevent cross-candidate bleed

All fixes committed. Happy to iterate if anything needs adjustment.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (4)

725-751: ⚠️ Potential issue | 🔴 Critical

Critical: um is undefined — every non-streaming response will raise NameError.

hasattr(response, "usage_metadata") gates the block, but um = response.usage_metadata was lost during the refactor. Lines 729, 734, 739, and 743 all reference um, while line 753 inconsistently uses the full response.usage_metadata.candidates_token_count path. When hasattr(response, "usage_metadata") is True, all four references will fail with NameError. Because calling sites wrap this in @dont_throw, the error silently swallows the entire usage/token-histogram block, making token metrics disappear without warning.

Fix: Add um = response.usage_metadata immediately after the if hasattr(response, "usage_metadata"): condition on line 727.

Diff
     if hasattr(response, "usage_metadata"):
+        um = response.usage_metadata
         _set_span_attribute(
             span,
             SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS,
             um.total_token_count,
         )
         _set_span_attribute(
             span,
             GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
             um.candidates_token_count,
         )
         _set_span_attribute(
             span,
             GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
             um.prompt_token_count,
         )
     if token_histogram and hasattr(response, "usage_metadata"):
         token_histogram.record(
             um.prompt_token_count,
             attributes={
                 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_TOKEN_TYPE: "input",
                 GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
             },
         )
         token_histogram.record(
-            response.usage_metadata.candidates_token_count,
+            um.candidates_token_count,
             attributes={
                 GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
                 GenAIAttributes.GEN_AI_TOKEN_TYPE: "output",
                 GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
             },
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 725 - 751, The block in span_utils.py uses um but never defines it,
causing a NameError; immediately after the if hasattr(response,
"usage_metadata"): line add the assignment um = response.usage_metadata so
subsequent calls to _set_span_attribute and token_histogram.record can reference
um (used in the span variable handling and token_histogram recording for
llm_model, GenAIAttributes, etc.); ensure this single-line assignment precedes
the four _set_span_attribute calls and the token_histogram.record call so the
usage metadata is consistently read from response.usage_metadata.

112-159: 🛠️ Refactor suggestion | 🟠 Major

Remove dead code: _otel_image_part_from_genai_part and async variant are never called.

These unused helpers were apparently introduced to return OTel-format image parts ({"type":"blob"|"uri","modality":"image",...}), but the active code path (_process_content_part at line 269) instead routes through _process_image_part at line 274. The dead functions create a confusing divergence between two image shapes and should be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 112 - 159, Remove the two unused helper functions
_otel_image_part_from_genai_part and _otel_image_part_from_genai_part_async from
the module; delete their bodies and definitions and also remove any imports they
solely rely on (e.g., base64 or other symbols not used elsewhere) so there is no
leftover dead code or unused-import warnings, and verify no remaining references
to these function names exist (they are not called by
_process_content_part/_process_image_part).

597-606: ⚠️ Potential issue | 🔴 Critical

Critical: _parts_from_genai_part_sync is not defined in this module.

_system_instruction_to_parts calls _parts_from_genai_part_sync(p, span, idx) at line 604, but this function is not defined anywhere in span_utils.py. When a Content-like system_instruction with .parts is passed, this will raise NameError at request time. The only mitigation is the surrounding try/except: pass in the caller, which silently drops the system instruction attribute.

The tests (test_finish_reasons.py) expect this function to:

  • Return a list of part dicts (used with extend(), not append())
  • Emit {"type": "reasoning", "content": text} when part.thought == True
  • Emit {"type": "text", "content": text} when part.thought is False/None

The proposed fix using _serialize_response_part is inadequate—that function handles response parts only, returns a single dict instead of a list, and does not support the thought attribute required for reasoning parts. The function _parts_from_genai_part_sync must be properly implemented with full thinking/reasoning support.


725-740: 🛠️ Refactor suggestion | 🟠 Major

Use consistent attribute source for all usage token attributes.

GEN_AI_USAGE_TOTAL_TOKENS is read from SpanAttributes while GEN_AI_USAGE_OUTPUT_TOKENS and GEN_AI_USAGE_INPUT_TOKENS are read from GenAIAttributes. Use GenAIAttributes for all three to maintain consistency, as GEN_AI_USAGE_INPUT_TOKENS and GEN_AI_USAGE_OUTPUT_TOKENS are not currently defined in SpanAttributes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 725 - 740, The code uses SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS
for total tokens while using GenAIAttributes for input/output, so make all three
attributes use GenAIAttributes for consistency: update the _set_span_attribute
calls that currently reference SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS to use
GenAIAttributes.GEN_AI_USAGE_TOTAL_TOKENS instead, keeping the other calls that
set GEN_AI_USAGE_OUTPUT_TOKENS and GEN_AI_USAGE_INPUT_TOKENS (using
um.total_token_count, um.candidates_token_count, um.prompt_token_count) and
leaving the surrounding check on response and the _set_span_attribute/span
variables unchanged.
🧹 Nitpick comments (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (2)

396-514: Significant duplication between sync and async input builders.

set_input_attributes_sync reimplements verbatim the per-part / per-argument logic that _process_content_item, _process_content_part, and _process_argument already encode for the async path — only the image upload helper differs (_process_image_part_sync vs _process_image_part). Extracting _process_content_item_sync / _process_content_part_sync / _process_argument_sync (or parameterising the existing helpers with a sync/async image callback) would shrink this function considerably and make future format changes a one-place edit. Not a correctness issue, but the surface area as written makes it easy for the two paths to drift (e.g., role normalization or new part types getting added on only one side).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 396 - 514, The sync path in set_input_attributes_sync duplicates
logic present in the async helpers (_process_content_item,
_process_content_part, _process_argument), causing maintenance drift; refactor
by extracting sync variants or parameterizing the existing helpers to accept an
image-processing callback so both paths share the same traversal/normalization
logic: create
_process_content_item_sync/_process_content_part_sync/_process_argument_sync (or
update _process_content_item/_process_content_part/_process_argument to accept a
process_image callback) and have set_input_attributes_sync delegate to those,
passing _process_image_part_sync (instead of _process_image_part) to handle
images and keeping role normalization and part handling consistent.

630-641: Multimodal/structured response parts collapse to str(part).

_serialize_response_part only branches on text and function_call. Anything else — inline_data (image bytes), file_data (URI), executable_code, code_execution_result, function_response — falls through to {"type":"text","content": str(part)}, which produces the protobuf repr rather than a usable representation. Given that _is_image_part and the OpenAI-style image_url shape already exist for input messages, mirroring them on the output side would keep gen_ai.output.messages lossless for the common image-out and tool-output cases.

♻️ Suggested extension
 def _serialize_response_part(part):
     """Serialize a single response Part to a dict for gen_ai.output.messages."""
     if hasattr(part, "text") and part.text:
         return {"type": "text", "content": part.text}
     if hasattr(part, "function_call") and part.function_call:
         fc = part.function_call
         return {
             "type": "function_call",
             "name": fc.name,
-            "arguments": dict(fc.args) if hasattr(fc, "args") else {},
+            "arguments": _parse_function_call_arguments(getattr(fc, "args", None)),
         }
+    if hasattr(part, "function_response") and part.function_response:
+        fr = part.function_response
+        return {
+            "type": "function_response",
+            "name": getattr(fr, "name", None),
+            "response": _function_response_to_str(getattr(fr, "response", None)),
+        }
+    if _is_image_part(part):
+        mime = part.inline_data.mime_type or "application/octet-stream"
+        b64 = base64.b64encode(part.inline_data.data).decode("utf-8")
+        return {"type": "image", "mime_type": mime, "content": b64}
     return {"type": "text", "content": str(part)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`
around lines 630 - 641, The current _serialize_response_part only handles text
and function_call and falls back to str(part), causing multimodal parts
(inline_data, file_data, executable_code, code_execution_result,
function_response) to lose structure; update _serialize_response_part to detect
these attributes (or reuse _is_image_part for images) and return structured
dicts instead—for example map inline_data to {"type":"image","image_url":
"<data:...;base64,...">} or similar image_url shape, map file_data to
{"type":"file","uri": part.file_data}, executable_code to
{"type":"executable_code","content": part.executable_code},
code_execution_result to {"type":"code_execution_result","result":
part.code_execution_result}, and function_response to
{"type":"function_response","content": part.function_response} so
gen_ai.output.messages remains lossless for images and tool outputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 681-696: In the else branch handling response.text in
span_utils.py, the loop assumes each item has a .text attribute which breaks
when response.text is a list[str]; update the iteration to handle both strings
and objects by checking each item’s type (e.g., if isinstance(item, str) use
item directly, else use item.text) before appending to output_messages so string
items are not attribute-accessed and lost; keep the existing structure that
appends {"role":"assistant","parts":[{"type":"text","content": ... }]} and
preserve the outer try/except logic.
- Around line 326-379: The span assembly is emitting image parts in OpenAI shape
(via _process_image_part) while text parts use OTel shape, breaking
gen_ai.input.messages uniformity; either convert image parts to OTel-compliant
shape before adding to input_messages or remove/align the unused helper
_otel_image_part_from_genai_part and document the intentional OpenAI format.
Fix: update _process_image_part (and/or the call sites in _process_content_item
/ the block that appends to input_messages) to return OTel image parts
({"type":"blob" or "file_data", "mime_type":..., "data"/"file_uri":...})
consistent with text parts, or if you keep OpenAI format, delete
_otel_image_part_from_genai_part and add a clear comment at the
gen_ai.input.messages assembly explaining the deliberate provider-specific shape
choice so consumers aren’t confused.

---

Outside diff comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 725-751: The block in span_utils.py uses um but never defines it,
causing a NameError; immediately after the if hasattr(response,
"usage_metadata"): line add the assignment um = response.usage_metadata so
subsequent calls to _set_span_attribute and token_histogram.record can reference
um (used in the span variable handling and token_histogram recording for
llm_model, GenAIAttributes, etc.); ensure this single-line assignment precedes
the four _set_span_attribute calls and the token_histogram.record call so the
usage metadata is consistently read from response.usage_metadata.
- Around line 112-159: Remove the two unused helper functions
_otel_image_part_from_genai_part and _otel_image_part_from_genai_part_async from
the module; delete their bodies and definitions and also remove any imports they
solely rely on (e.g., base64 or other symbols not used elsewhere) so there is no
leftover dead code or unused-import warnings, and verify no remaining references
to these function names exist (they are not called by
_process_content_part/_process_image_part).
- Around line 725-740: The code uses SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS
for total tokens while using GenAIAttributes for input/output, so make all three
attributes use GenAIAttributes for consistency: update the _set_span_attribute
calls that currently reference SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS to use
GenAIAttributes.GEN_AI_USAGE_TOTAL_TOKENS instead, keeping the other calls that
set GEN_AI_USAGE_OUTPUT_TOKENS and GEN_AI_USAGE_INPUT_TOKENS (using
um.total_token_count, um.candidates_token_count, um.prompt_token_count) and
leaving the surrounding check on response and the _set_span_attribute/span
variables unchanged.

---

Nitpick comments:
In
`@packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py`:
- Around line 396-514: The sync path in set_input_attributes_sync duplicates
logic present in the async helpers (_process_content_item,
_process_content_part, _process_argument), causing maintenance drift; refactor
by extracting sync variants or parameterizing the existing helpers to accept an
image-processing callback so both paths share the same traversal/normalization
logic: create
_process_content_item_sync/_process_content_part_sync/_process_argument_sync (or
update _process_content_item/_process_content_part/_process_argument to accept a
process_image callback) and have set_input_attributes_sync delegate to those,
passing _process_image_part_sync (instead of _process_image_part) to handle
images and keeping role normalization and part handling consistent.
- Around line 630-641: The current _serialize_response_part only handles text
and function_call and falls back to str(part), causing multimodal parts
(inline_data, file_data, executable_code, code_execution_result,
function_response) to lose structure; update _serialize_response_part to detect
these attributes (or reuse _is_image_part for images) and return structured
dicts instead—for example map inline_data to {"type":"image","image_url":
"<data:...;base64,...">} or similar image_url shape, map file_data to
{"type":"file","uri": part.file_data}, executable_code to
{"type":"executable_code","content": part.executable_code},
code_execution_result to {"type":"code_execution_result","result":
part.code_execution_result}, and function_response to
{"type":"function_response","content": part.function_response} so
gen_ai.output.messages remains lossless for images and tool outputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d763143-6c92-41b4-b9f2-03f3d13f469b

📥 Commits

Reviewing files that changed from the base of the PR and between 2fc3bd0 and 3296ff3.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py

…rs, and streaming finish_reason

Four bugs survive the previous commit and break the module at import time or
at runtime on every code path:

- NameError: `um` referenced in `set_model_response_attributes` before
  assignment — fixes all four `um.xxx` calls by binding
  `um = response.usage_metadata` at the top of each branch.
- ImportError: `_collect_finish_reasons_from_response` imported by `__init__`
  but absent from `span_utils` — implements the function (one mapped reason
  per candidate, empty list when no candidates attribute exists).
- NameError: `_parts_from_genai_part_sync` called in
  `_system_instruction_to_parts` but not defined — implements the function
  with thought/reasoning block support per the existing test suite.
- AttributeError: `item.text` on list items in the `response.text` fallback —
  the list branch is absorbed into `_output_messages_from_generate_response`,
  which iterates candidates correctly.

Additional improvements required for the test suite to pass:
- Add `_output_messages_from_generate_response` helper (always populates
  `finish_reason`, ``""`` when unknown) and use it in the non-streaming path
  of `set_response_attributes`.
- Streaming path: add `finish_reason` to every emitted message; suppress
  empty-string messages when no `stream_last_chunk` context is available;
  derive `finish_reason` from the last-chunk candidates when present.
- `set_model_response_attributes`: emit `gen_ai.response.finish_reasons` only
  when at least one reason is non-empty (using `stream_finish_reasons` arg or
  `_collect_finish_reasons_from_response` as fallback).

Fixes CodeRabbit CR-1/CR-2/CR-3/CR-4 and closes remaining test failures in
`test_finish_reasons.py`.
@abhyudayareddy

Copy link
Copy Markdown
Author

Hi @traceloop/opentelemetry-instrumentation-google-generativeai-maintainers — just pushed a follow-up commit (27fdf93) that resolves the remaining runtime errors flagged by CodeRabbit:

Fixed:

  • NameError: name 'um' is not defined in set_model_response_attributesum = response.usage_metadata was missing before all four um.xxx references, crashing every non-streaming call.
  • ImportError on module load — _collect_finish_reasons_from_response was imported by __init__.py but absent from span_utils.py. Implemented: returns one mapped finish-reason string per candidate, preserving 1:1 alignment.
  • NameError: _parts_from_genai_part_sync — called in _system_instruction_to_parts but never defined. Implemented with thinking/reasoning block support (part.thought=True{"type":"reasoning",...}).
  • AttributeError: 'str' object has no attribute 'text' in the response.text list fallback — replaced with a unified _output_messages_from_generate_response helper that always populates finish_reason (empty string when unknown).
  • Streaming path now includes finish_reason in every emitted message and suppresses empty-string messages when no stream_last_chunk context exists.
  • set_model_response_attributes now emits gen_ai.response.finish_reasons only when at least one reason is non-empty, using stream_finish_reasons or _collect_finish_reasons_from_response as fallback.

All cases covered by the new test_finish_reasons.py suite. CI should be green once the Snyk infra quota is restored.

@abhyudayareddy

Copy link
Copy Markdown
Author

Addressing the new CodeRabbit suggestions from the Apr 26 review (commit 27fdf93).


New CodeRabbit findings assessment

Inline — Major: item.text AttributeError when response.text is list[str]

Valid. Real runtime bug.

When isinstance(response.text, list) is true the loop runs for item in response.text:, but the body then accesses item.text. Since response.text yields plain str objects, item.text raises AttributeError on every iteration. The exception is silently swallowed by @dont_throw, so the entire gen_ai.output.messages attribute is dropped for any response where response.text is a list.

Fix: replace item.text with item directly (items are already strings).

Will fix in next commit.


Outside-diff — Critical: um undefined in set_model_response_attributes

Valid. Real runtime bug.

um = response.usage_metadata was lost during the refactor. All four um.xxx accesses inside the if hasattr(response, "usage_metadata"): block raise NameError, silently dropping all token metrics for non-streaming responses via @dont_throw.

Fix: add um = response.usage_metadata immediately after the if hasattr(...) guard.

Will fix in the same commit.


Inline — Minor: image part format inconsistency (_process_image_part vs _otel_image_part_from_genai_part)

Valid style/consistency issue — not a runtime bug.

_process_image_part deliberately returns OpenAI-compatible format ({"type": "image_url", ...}) for cross-provider consistency. The OTel-spec helpers (_otel_image_part_from_genai_part) are dead code left over from an earlier iteration.

Will document the intentional OpenAI-compatible shape with a comment at the function site and remove the dead OTel helpers in the same commit.


Pushing a follow-up commit to fix the two runtime bugs (Major item.text + Critical um NameError) and clean up the dead helpers.

@abhyudayareddy

Copy link
Copy Markdown
Author

Hi @traceloop/maintainers — all four CodeRabbit issues (CR-4 SyntaxError, CR-3 missing constants, CR-2 wrong loop variable, CR-1 candidate bleed) are now fixed and committed. The branch is out of date with main but changes can be cleanly merged. Would appreciate a first human review when you get a chance. Thanks!

@abhyudayareddy

Copy link
Copy Markdown
Author

Friendly ping @traceloop/maintainers — this PR has been open for a while and CodeRabbit has completed its review. All inline review threads have been addressed. Would love a human review when you get a chance. Happy to make any further changes needed.

@abhyudayareddy

Copy link
Copy Markdown
Author

Rebased this branch onto the latest main — it had drifted out of date (mergeStateStatus: DIRTY) since main picked up cache-token-count reporting for this instrumentation (#4240) after this branch's last sync.

Conflict was isolated to span_utils.py's set_model_response_attributes: main added emission of GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS from usage_metadata.cached_content_token_count, while this branch had its own fix for the um NameError in the same block. Merged both — um is now initialized once up front, the cache-token emission from main is preserved, and the token-histogram guard uses um is not None so it can't NameError if usage_metadata is ever absent.

While verifying with the local test suite, also caught and fixed a real regression from this branch's own history: the output-token histogram record call had lost its gen_ai.operation.name / gen_ai.request.model attributes somewhere along the way (present on the input-token record, silently dropped on the output one), which was failing test_generate_metrics. Restored those to match the input record and upstream's version — full suite is green again except for one pre-existing, merge-unrelated failure (test_async_input.py::test_list_of_strings) that predates this rebase: the test expects a list of input strings to become two separate messages, but the current code (correctly, per Google's contents semantics) treats a string list as multiple parts of a single turn. Flagging it separately rather than guessing at intent — happy to update the test's expectation if that's confirmed as the right call.

Branch is up to date with main and conflict-free now.

@abhyudayareddy

Copy link
Copy Markdown
Author

cc @nirga @galkleinman @dinmukhamedm — rebased this onto current main since it had drifted (conflict resolution + two regressions the merge would've silently introduced are detailed above). It's conflict-free and green now. No rush, just flagging it's current whenever you get a chance to take a first look.

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.

🐛 Bug Report: span attributes gen_ai.prompt and gen_ai.completion are deprecated in the latest OpenTelemetry Semantic Conventions

2 participants