Skip to content

[OPIK-7717] [BE] fix: map OTel semconv provider names to Opik canonical providers - #7909

Open
awkoy wants to merge 5 commits into
mainfrom
OPIK-7717-otel-provider-aliases
Open

[OPIK-7717] [BE] fix: map OTel semconv provider names to Opik canonical providers#7909
awkoy wants to merge 5 commits into
mainfrom
OPIK-7717-otel-provider-aliases

Conversation

@awkoy

@awkoy awkoy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Details

OTel instrumentation reports the provider via gen_ai.system using the semantic-convention vocabulary, which spells several providers differently from Opik's price table. OpenTelemetryMapper copied the value verbatim, so vertex_ai never matched the google_vertexai price row and the span silently cost $0 — even though the same model prices correctly under the canonical name.

Same model, same usage, only the wire value differs:

gen_ai.system stored provider cost
google + aiplatform host google_vertexai $0.001000
vertex_ai vertex_ai $0.000000

New GenAiProviderAliasResolver normalizes the semconv vocabulary at ingestion:

vertex_ai, gcp.vertex_ai        -> google_vertexai
gcp.gemini                      -> google_ai
aws.bedrock                     -> bedrock
az.ai.openai, azure.ai.openai   -> azure
mistral_ai                      -> mistral
x_ai                            -> xai

It also starts reading gen_ai.provider.name, which replaced the deprecated gen_ai.system and wasn't read at all. When a span carries both, gen_ai.system wins — so no span that resolves a provider today can change behaviour.

Why at ingestion, not at cost lookup: the raw value is also persisted to the provider column, so fixing only the lookup would leave filtering and grouping split across two names for identical traffic.

Deliberately not aliased:

  • gcp.gen_ai — semconv defines it as "specific backend is unknown", so it's disambiguated by server.address like the existing generic google.
  • azure.ai.inference — fronts both Azure OpenAI and Foundry models (Claude, Llama), which LiteLLM prices under a separate azure_ai provider; aliasing it to azure would price a Foundry model against the OpenAI table.
  • gemini, vertex-ai — already price correctly today; rewriting them would change stored data on a working path and silently break saved provider filters.

⚠️ Forward-only: cost is computed once at ingestion and stored, with no re-pricing path. Spans already ingested with cost 0 are not corrected — only new spans. Flagged on the ticket so support can set expectations.

Change checklist

  • User facing
  • Documentation update

Issues

  • Resolves #
  • OPIK-7717

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: Research, implementation, tests and local end-to-end verification.
  • Human verification: Author reviewed the diff and reproduction results; every scoping decision (alias set, fix site, precedence rule, exclusions) was decided by the author before implementation.

Testing

mvn test -Dtest='OtelProviderCostPipelineTest,OpenTelemetryMapperTest,CostServiceTest'
→ Tests run: 269, Failures: 0, Errors: 0

13 of the 18 pipeline cases failed before the fix, so that suite is a real regression test rather than a restatement of current behaviour.

Covered: every alias; both gcp.gen_ai branches; padded values; hosts merely embedding a marker (must not classify as Vertex); hosts with port / scheme / path (must still resolve); the precedence guard (gen_ai.system=xai + gen_ai.provider.name=x_aixai); and the two deliberate exclusions.

Ingestion-path regression test

All of the above stops at the mapper and CostService in-process. Nothing posted OTLP over HTTP and then read the persisted span back, so a regression in the resource/ingestion wiring could have landed with every test still green. OpenTelemetryResourceTest#testProviderVocabularyIsAliasedAndPriced closes that: real OTLP protobuf to /v1/private/otel/v1/traces, then the persisted span read back, asserting the stored provider and a non-zero total_estimated_cost.

mvn test -Dtest='OpenTelemetryResourceTest$ApiKey#testProviderVocabularyIsAliasedAndPriced'
→ Tests run: 8, Failures: 0, Errors: 0

7 of the 8 cases fail against main, so this is a real regression test — including gen_ai.provider.name, which resolved to a null provider before this PR. The eighth (google + Vertex host) already worked and is carried as a guard against regressing it.

This also fills a gap the QA test radar flagged on this PR: nothing in the tagged e2e estate posts to the OTel endpoint, and the one spec tagged traces.span-model-cost-tokens seeds provider and total_cost through the SDK, so it would pass even if every alias here were wrong. Adding the coverage in the backend resource test asserts the exact derived values rather than the UI's rounded <$0.01, and needs no new QA-owned taxonomy capability.

End-to-end, manually, against a backend built from this branch: real OTLP spans posted to /v1/private/otel/v1/traces, then persisted spans read back through the public API — 22/22 pass. The ticket's exact case (vertex_ai + gemini-3.1-flash-lite) stores google_vertexai and costs $0.001000, confirmed at both span and trace level.

Documentation

None needed — internal ingestion fix with no API, configuration or SDK surface change. Spans that showed $0 simply start showing the correct cost.

…al providers

Custom OTel instrumentation reports the provider via `gen_ai.system` using the
semantic-convention vocabulary, which spells several providers differently from
Opik's price-table vocabulary. OpenTelemetryMapper copied the value verbatim into
the span's `provider`, so `vertex_ai` never reached the `google_vertexai` price
row and the span silently cost 0 — even though `gemini-3.1-flash-lite` is priced
correctly under the canonical name. Only the generic `google` value was special
cased (GoogleProviderResolver); CostService aliased the hyphenated `vertex-ai`
but not the OTel-standard underscore form.

Add GenAiProviderAliasResolver for the unambiguous 1:1 renames, each confirmed to
price at 0 before and correctly after:

    vertex_ai, gcp.vertex_ai -> google_vertexai
    gcp.gemini               -> google_ai
    aws.bedrock              -> bedrock
    az.ai.openai,
    azure.ai.openai          -> azure
    mistral_ai               -> mistral
    x_ai                     -> xai

Both spellings are carried where the semconv renamed a value but instrumentation
still emits the old one (`vertex_ai` -> `gcp.vertex_ai`, `az.ai.openai` ->
`azure.ai.openai`). The rewrite happens at ingestion rather than at cost lookup
because the raw value is also persisted to the `provider` column, so fixing only
the lookup would leave filtering and grouping split across two names for
identical traffic.

Values naming more than one backend are deliberately excluded. `gcp.gen_ai`
("specific backend is unknown" per the semconv) joins the generic `google` in
GoogleProviderResolver's server.address disambiguation. `azure.ai.inference` /
`az.ai.inference` front either Azure OpenAI or Foundry models such as Claude and
Llama, which LiteLLM prices under a separate `azure_ai` provider absent from
CostService.PROVIDERS_MAPPING; aliasing them to `azure` would price a Foundry
model against the OpenAI table.

Also map `gen_ai.provider.name`, which replaced the now-deprecated
`gen_ai.system` and was not read at all, leaving newer instrumentation with no
provider. Instrumentations mid-migration emit both attributes with differing
vocabularies (`xai` vs `x_ai`), so `gen_ai.system` stays authoritative and the
new attribute only fills in when absent — keeping this strictly additive for
spans that already resolve a provider.

Note this is forward-only: cost is computed once at ingestion and stored, so
spans already persisted with cost 0 are not re-priced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@awkoy
awkoy requested a review from a team as a code owner August 19, 2026 12:44
@github-actions github-actions Bot added java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. 🟠 size/L labels Aug 19, 2026
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
☕ spotless — java backend Format Java code 3.70s
Total (1 ran) 3.70s
⏭️ 42 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️

@CometActions

CometActions commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

No test needed here.

This PR already ships the test that would catch it: OpenTelemetryResourceTest.testProviderVocabularyIsAliasedAndPriced POSTs real OTLP to /v1/private/otel/v1/traces and asserts both the persisted provider and a non-zero total_estimated_cost for all eight aliases, which is exactly the user-visible outcome (a Vertex span showing $0 in Logs). The UI side is a pass-through of that number, and trace-explore/trace-spans-depth.spec.ts (@cap:traces.span-model-cost-tokens) already asserts the span panel renders model, tokens and cost. Nothing for a Playwright spec to add here. Worth noting separately: the e2e estate has no OTLP ingestion coverage at all and taxonomy.yaml has no capability for it — a gap for the coverage radar, not something to hang on this PR.

Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

Re-checked after a push on 20 Aug 10:53 UTC.

@CometActions CometActions added the test-environment Deploy Opik adhoc environment label Aug 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Test environment deployment process has started

Phase 1: Deploying base version 2.2.32-6367 (from main branch) if environment doesn't exist
Phase 2: Building new images from PR branch OPIK-7717-otel-provider-aliases
Phase 3: Will deploy newly built version after build completes

You can monitor the progress here.

@CometActions

Copy link
Copy Markdown
Collaborator

Test environment is now available!

To configure additional Environment variables for your environment, run [Deploy Opik AdHoc Environment workflow] (https://github.com/comet-ml/comet-deployment/actions/workflows/deploy_opik_adhoc_env.yaml)

Access Information

The deployment has completed successfully and the version has been verified.

…hosts on a domain boundary

Addresses review findings on the OTel provider aliasing change.

A blank or non-string `gen_ai.provider.name` yields "" from getStringValue(), and
the fallback assigned it unconditionally — persisting an empty provider on a span
that previously carried none at all. Require both sides to be non-blank.

GoogleProviderResolver matched its host markers with `contains`, so an address
such as `evil-aiplatform.googleapis.com.attacker.test` classified as Vertex AI.
Reduce `server.address` to its bare host and match on a suffix instead. Scheme,
path, numeric port and a trailing FQDN dot are stripped first so well-formed
addresses are not rejected by the stricter match, and an IPv6 literal is not
truncated at its last colon.

Also collapse the two DEBUG records the unrecognized-host path emitted into one.

Tests: 266 pass (13 new), covering embedded-marker hosts that must not classify
as Vertex, well-formed addresses with port/scheme/path/FQDN dot that must still
resolve, and blank provider-name values that must neither persist nor override.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ank-provider tests

Addresses follow-up review findings.

GenAiProviderAliasResolver matched aliases on the trimmed value but returned the
original on a miss, so a padded " openai " persisted verbatim and missed the
price-table lookup, which keys on the exact provider string. Return the trimmed
value either way so both paths are consistent. Case is deliberately left alone:
lowercasing would rewrite stored providers that are usable for filtering today.

Also convert the blank-provider regression test to a @ParameterizedTest, matching
the surrounding provider tests, and cover padded values in both directions.

Tests: 269 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CometActions

Copy link
Copy Markdown
Collaborator

🌙 Nightly cleanup: The test environment for this PR (pr-7909) has been cleaned up to free cluster resources. PVCs are preserved — re-deploy to restore the environment.

@CometActions CometActions removed the test-environment Deploy Opik adhoc environment label Aug 20, 2026
alexkuzmik and others added 2 commits August 20, 2026 12:40
The existing alias coverage stops at the mapper and CostService in-process.
Nothing posted OTLP over HTTP and then read the persisted span back, so a
regression in the resource/ingestion wiring could land with every test green.

Parameterized over the alias table plus the gen_ai.provider.name fallback and
one ambiguous google + server.address case. 7 of the 8 fail against main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 12

222 tests   221 ✅  4m 3s ⏱️
 29 suites    0 💤
 29 files      1 ❌

For more details on these failures, see this check.

Results for commit c0931dc.

♻️ This comment has been updated with latest results.

Comment on lines +431 to +434
Stream<Arguments> testProviderVocabularyIsAliasedAndPriced() {
return Stream.of(
// The OPIK-7717 report: stored verbatim, 'vertex_ai' matched no price row and cost 0.
arguments("vertex_ai", "gen_ai.system", "vertex_ai", "gemini-3.1-flash-lite", null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Backend test violates naming convention

testProviderVocabularyIsAliasedAndPriced uses a test... prefix, and its implicit @MethodSource resolves by that name, so renaming only the test breaks discovery before any cases run. Should we rename the test and factory together (or use @MethodSource("...")), as apps/opik-backend/AGENTS.md and .agents/skills/opik-backend/testing.md require?

Severity web_search

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/OpenTelemetryResourceTest.java`
around lines 431-434, rename the `testProviderVocabularyIsAliasedAndPriced` parameter
source and its corresponding parameterized test to descriptive method-style names
without the `test` prefix. Keep both names identical, or add an explicit `@MethodSource`
name, because the current implicit factory lookup depends on matching method names.

Comment on lines +445 to +447
// Names no backend on its own, so it is resolved from the endpoint host instead.
arguments("google + vertex host", "gen_ai.system", "google", "gemini-2.5-flash-lite",
"us-east1-aiplatform.googleapis.com", "google_vertexai"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fallback rationale is unclear

Endpoints without a backend name resolve from the host instead, so let's verify that path works end-to-end.

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/OpenTelemetryResourceTest.java
around lines 445-447, update the comment in the testProviderVocabularyIsAliasedAndPriced
method. Replace “Names no backend on its own” with “Names no backend on their
own” so the sentence is grammatically correct and clearly explains that the provider
is resolved from the endpoint host.

Comment on lines +494 to +497
assertThat(persistedSpan.totalEstimatedCost())
.as("cost stored for model %s under provider %s", model, expectedProvider)
.isNotNull()
.isGreaterThan(BigDecimal.ZERO);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

totalEstimatedCost() only checks that the value is greater than zero, so an incorrect pricing row or gemini-3.1-flash-lite fallback under google_vertexai can pass — should we add an expected cost (e.g. $0.001000 for the ticket case) to each Arguments entry and assert exact equality with the existing scale-aware BigDecimal comparison?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/OpenTelemetryResourceTest.java
around lines 494-497, update `testProviderVocabularyIsAliasedAndPriced` and its
method-source cases so the pricing regression test validates the exact expected cost
instead of merely requiring a positive value. Add an expected `BigDecimal` cost argument
for every provider/model case (calculated deterministically for 1,000 input and 500
output tokens from the loaded pricing table, e.g. `$0.001000` for the documented ticket
case), and assert `persistedSpan.totalEstimatedCost()` equals that value exactly, using
the same scale-aware BigDecimal comparison convention as the existing cost tests.

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 14

 30 files   30 suites   5m 27s ⏱️
339 tests 337 ✅ 2 💤 0 ❌
321 runs  319 ✅ 2 💤 0 ❌

Results for commit c0931dc.

♻️ This comment has been updated with latest results.

*/
@UtilityClass
@Slf4j
public class GenAiProviderAliasResolver {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should probably abstract all those resolvers and find a way select the ones that must be applied

Comment on lines +85 to +104
private static String extractHost(String serverAddress) {
String host = serverAddress;

int scheme = host.indexOf("://");
if (scheme >= 0) {
host = host.substring(scheme + 3);
}
int path = host.indexOf('/');
if (path >= 0) {
host = host.substring(0, path);
}
// Only strip a genuine numeric port, so an IPv6 literal isn't truncated at its last colon.
int port = host.lastIndexOf(':');
if (port >= 0 && port < host.length() - 1
&& host.substring(port + 1).chars().allMatch(Character::isDigit)) {
host = host.substring(0, port);
}

return StringUtils.removeEnd(host, ".");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Better to use a URLParser

@thiagohora thiagohora left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review of the provider-alias changes. Two findings worth acting on (span mistyping, Anthropic-on-Vertex pricing), plus a doc gap and a test that doesn't exercise what it claims.

Checked and clean: rule precedence/placement (gen_ai.system is the only other PROVIDER rule; no earlier ruleset matches the new key), the blank guards avoiding persisted "", the fallback ordering that keeps gen_ai.provider.name=elastic routing through ElasticInferenceServiceResolver, extractHost scheme/path/port/IPv6/trailing-dot handling and the containsendsWith tightening, the alias set against the OTel GenAI registry, every model asserted in the new tests against model_prices_and_context_window.json, and the coverage lost by deleting GoogleProviderOtelPipelineTest (carried by OtelProviderCostPipelineTest).

private static final Map<String, String> ALIASES = Map.ofEntries(
// Legacy spellings the semconv renamed but instrumentation still emits (OPIK-7717):
// `vertex_ai` -> `gcp.vertex_ai`, `az.ai.openai` -> `azure.ai.openai`.
Map.entry("vertex_ai", GoogleProviderResolver.GOOGLE_VERTEX_AI),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

correctnessvertex_aigoogle_vertexai isn't the unambiguous 1:1 rename the javadoc describes.

Vertex fronts Anthropic models, which Opik prices under a different canonical provider: anthropic_vertexai, keyed vertex_ai-anthropic_models in CostService.PROVIDERS_MAPPING (asserted in CostServiceTest:143).

Failure: gen_ai.system=vertex_ai + gen_ai.request.model=claude-haiku-4-5 now stores google_vertexai. findModelPrice("claude-haiku-4-5", "google_vertexai") misses every fallback — the prefix fallback needs a / in the model name, and PROVIDERS_MAPPING has no vertex_ai key — so the span still costs $0, and is now additionally mislabeled and grouped/filtered as a Google provider.

This is precisely the multi-backend ambiguity for which azure.ai.inference was deliberately excluded, so the exclusion rationale is applied inconsistently here. Consider disambiguating by model family (claude*anthropic_vertexai), or at minimum documenting the gap. Same applies to the gcp.vertex_ai entry below.

resolved = GOOGLE_AI;
log.debug("Provider 'google' with unrecognized server.address '{}', defaulting to '{}'",
serverAddress, resolved);
recognized = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low — with gcp.gen_ai added to AMBIGUOUS_PROVIDERS, this default branch now affirmatively mislabels rather than leaving the value alone.

Previously a gcp.gen_ai span with no (or an unrecognized) server.address was stored verbatim as gcp.gen_ai — cost 0, but obviously unmapped. It now stores google_ai, so a Vertex call from an instrumentation that omits server.address is persisted as Gemini Developer API traffic: provider filtering and grouping silently attribute it to the wrong backend, and if the two price tables ever diverge the cost is wrong rather than merely absent.

The class javadoc carries this caveat for google; it should state it for gcp.gen_ai too.

}

@ParameterizedTest(name = "[{index}] {0} -> google_vertexai")
@CsvSource({"VERTEX_AI", "Vertex_Ai", " vertex_ai "})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

test-coverage — the whitespace half of this test is never exercised.

JUnit's @CsvSource defaults to ignoreLeadingAndTrailingWhitespace = true for unquoted columns, so " vertex_ai " reaches the mapper as vertex_ai and merely duplicates the plain lowercase case. A regression in whitespace handling would still pass.

Quote it — "' vertex_ai '" — as paddedProviderIsStoredTrimmed correctly does.

// `gen_ai.system` authoritative when present; see PROVIDER_NAME_ATTR there.
OpenTelemetryMappingRule.builder()
.rule(PROVIDER_NAME_ATTR).source(SOURCE).outcome(OpenTelemetryMappingRule.Outcome.PROVIDER)
.spanType(SpanType.llm).build(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

correctness.spanType(SpanType.llm) on this rule will mistype execute_tool spans.

Unlike gen_ai.system, gen_ai.provider.name is defined in current semconv on execute_tool and invoke_agent spans too, not only inference spans. enrichSpanWithAttributes applies rule.getSpanType() unconditionally while walking the attribute list, so the last spanType-bearing rule wins.

Failure: an execute_tool span from an instrumentation that has fully migrated to the new attribute (so it emits no gen_ai.system) carrying only gen_ai.tool.name (METADATA, no spanType) is typed llm instead of tool — and the type also flips whenever gen_ai.provider.name is iterated after gen_ai.tool.call.arguments. Before this PR the key had no rule at all, so such a span kept tool/general deterministically.

invoke_agent is protected by the explicit operation-name guard in OpenTelemetryMapper (~L281); execute_tool has no equivalent. Either drop spanType from this rule, or extend that guard to cover execute_tool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend java Pull requests that update Java code 🔴 size/XL tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants