From b92a08101c3de745c43f7cb06332d3a297d3ae3c Mon Sep 17 00:00:00 2001 From: roroghost17 Date: Fri, 22 May 2026 16:08:59 +0530 Subject: [PATCH] refactor: changes attribute keys in OTEL plugin to follow semantic conventions --- core/bifrost.go | 57 ++++++++++++++------- core/mcp/pluginpipeline.go | 46 ++++++++++++++++- core/schemas/otelconv.go | 50 +++++++++++++++++++ core/schemas/trace.go | 79 ++++++++++++++++++++++++++--- framework/tracing/llmspan.go | 97 +++++++++++++++++++++++++----------- plugins/otel/converter.go | 5 +- 6 files changed, 275 insertions(+), 59 deletions(-) create mode 100644 core/schemas/otelconv.go diff --git a/core/bifrost.go b/core/bifrost.go index 5a94914d0b5..f0099310a81 100644 --- a/core/bifrost.go +++ b/core/bifrost.go @@ -4573,7 +4573,8 @@ func (bifrost *Bifrost) handleRequest(ctx *schemas.BifrostContext, req *schemas. // Start span for fallback attempt tracer := bifrost.getTracer() spanCtx, handle := tracer.StartSpan(ctx, fmt.Sprintf("fallback.%s.%s", fallback.Provider, fallback.Model), schemas.SpanKindFallback) - tracer.SetAttribute(handle, schemas.AttrProviderName, string(fallback.Provider)) + tracer.SetAttribute(handle, schemas.AttrProviderName, schemas.OTelProviderName(fallback.Provider)) + tracer.SetAttribute(handle, schemas.AttrBifrostProviderName, string(fallback.Provider)) // raw Bifrost short name, mirrors canonical gen_ai.provider.name tracer.SetAttribute(handle, schemas.AttrRequestModel, fallback.Model) tracer.SetAttribute(handle, "fallback.index", i+1) ctx.SetValue(schemas.BifrostContextKeySpanID, spanCtx.Value(schemas.BifrostContextKeySpanID)) @@ -4654,7 +4655,8 @@ func (bifrost *Bifrost) handleStreamRequest(ctx *schemas.BifrostContext, req *sc // Start span for fallback attempt tracer := bifrost.getTracer() spanCtx, handle := tracer.StartSpan(ctx, fmt.Sprintf("fallback.%s.%s", fallback.Provider, fallback.Model), schemas.SpanKindFallback) - tracer.SetAttribute(handle, schemas.AttrProviderName, string(fallback.Provider)) + tracer.SetAttribute(handle, schemas.AttrProviderName, schemas.OTelProviderName(fallback.Provider)) + tracer.SetAttribute(handle, schemas.AttrBifrostProviderName, string(fallback.Provider)) // raw Bifrost short name, mirrors canonical gen_ai.provider.name tracer.SetAttribute(handle, schemas.AttrRequestModel, fallback.Model) tracer.SetAttribute(handle, "fallback.index", i+1) ctx.SetValue(schemas.BifrostContextKeySpanID, spanCtx.Value(schemas.BifrostContextKeySpanID)) @@ -5268,10 +5270,11 @@ func executeRequestWithRetries[T any]( var keyHandle schemas.SpanHandle if keyTracer != nil { keySpanCtx, keyHandle = keyTracer.StartSpan(ctx, "key.selection", schemas.SpanKindInternal) - keyTracer.SetAttribute(keyHandle, schemas.AttrProviderName, string(providerKey)) + keyTracer.SetAttribute(keyHandle, schemas.AttrProviderName, schemas.OTelProviderName(providerKey)) + keyTracer.SetAttribute(keyHandle, schemas.AttrBifrostProviderName, string(providerKey)) // raw Bifrost short name, mirrors canonical gen_ai.provider.name keyTracer.SetAttribute(keyHandle, schemas.AttrRequestModel, model) if attempts > 0 { - keyTracer.SetAttribute(keyHandle, "retry.count", attempts) + keyTracer.SetAttribute(keyHandle, schemas.AttrLegacyRetryCount, attempts) } } @@ -5341,50 +5344,68 @@ func executeRequestWithRetries[T any]( } var spanName string var spanKind schemas.SpanKind + otelOp := schemas.OTelOperationName(requestType) if attempts > 0 { spanName = fmt.Sprintf("retry.attempt.%d", attempts) spanKind = schemas.SpanKindRetry } else { - spanName = "llm.call" + // Span name format per OTel GenAI semconv: "{operation} {model}". + spanName = fmt.Sprintf("%s %s", otelOp, model) spanKind = schemas.SpanKindLLMCall } spanCtx, handle := tracer.StartSpan(ctx, spanName, spanKind) - tracer.SetAttribute(handle, schemas.AttrProviderName, string(providerKey)) + tracer.SetAttribute(handle, schemas.AttrProviderName, schemas.OTelProviderName(providerKey)) + tracer.SetAttribute(handle, schemas.AttrBifrostProviderName, string(providerKey)) // raw Bifrost short name, mirrors canonical gen_ai.provider.name tracer.SetAttribute(handle, schemas.AttrRequestModel, model) - tracer.SetAttribute(handle, "request.type", string(requestType)) + tracer.SetAttribute(handle, schemas.AttrOperationName, otelOp) + tracer.SetAttribute(handle, schemas.AttrLegacyRequestType, string(requestType)) // legacy: replaced by gen_ai.operation.name if attempts > 0 { - tracer.SetAttribute(handle, "retry.count", attempts) + tracer.SetAttribute(handle, schemas.AttrLegacyRetryCount, attempts) // legacy: bare key with no semconv prefix } // Add context-related attributes (selected key, virtual key, team, customer, etc.) + // Each AttrXxx (gen_ai.*) emission below is LEGACY namespace pollution: the + // Bifrost-internal concept does not belong under gen_ai.*. The bifrost.* mirrors + // are the canonical home going forward; once all dashboards migrate, drop the + // gen_ai.* lines (grep for "// legacy:" in this block). if selectedKeyID, ok := ctx.Value(schemas.BifrostContextKeySelectedKeyID).(string); ok && selectedKeyID != "" { - tracer.SetAttribute(handle, schemas.AttrSelectedKeyID, selectedKeyID) + tracer.SetAttribute(handle, schemas.AttrSelectedKeyID, selectedKeyID) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostSelectedKeyID, selectedKeyID) } if selectedKeyName, ok := ctx.Value(schemas.BifrostContextKeySelectedKeyName).(string); ok && selectedKeyName != "" { - tracer.SetAttribute(handle, schemas.AttrSelectedKeyName, selectedKeyName) + tracer.SetAttribute(handle, schemas.AttrSelectedKeyName, selectedKeyName) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostSelectedKeyName, selectedKeyName) } if virtualKeyID, ok := ctx.Value(schemas.BifrostContextKeyGovernanceVirtualKeyID).(string); ok && virtualKeyID != "" { - tracer.SetAttribute(handle, schemas.AttrVirtualKeyID, virtualKeyID) + tracer.SetAttribute(handle, schemas.AttrVirtualKeyID, virtualKeyID) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostVirtualKeyID, virtualKeyID) } if virtualKeyName, ok := ctx.Value(schemas.BifrostContextKeyGovernanceVirtualKeyName).(string); ok && virtualKeyName != "" { - tracer.SetAttribute(handle, schemas.AttrVirtualKeyName, virtualKeyName) + tracer.SetAttribute(handle, schemas.AttrVirtualKeyName, virtualKeyName) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostVirtualKeyName, virtualKeyName) } if teamID, ok := ctx.Value(schemas.BifrostContextKeyGovernanceTeamID).(string); ok && teamID != "" { - tracer.SetAttribute(handle, schemas.AttrTeamID, teamID) + tracer.SetAttribute(handle, schemas.AttrTeamID, teamID) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostTeamID, teamID) } if teamName, ok := ctx.Value(schemas.BifrostContextKeyGovernanceTeamName).(string); ok && teamName != "" { - tracer.SetAttribute(handle, schemas.AttrTeamName, teamName) + tracer.SetAttribute(handle, schemas.AttrTeamName, teamName) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostTeamName, teamName) } if customerID, ok := ctx.Value(schemas.BifrostContextKeyGovernanceCustomerID).(string); ok && customerID != "" { - tracer.SetAttribute(handle, schemas.AttrCustomerID, customerID) + tracer.SetAttribute(handle, schemas.AttrCustomerID, customerID) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostCustomerID, customerID) } if customerName, ok := ctx.Value(schemas.BifrostContextKeyGovernanceCustomerName).(string); ok && customerName != "" { - tracer.SetAttribute(handle, schemas.AttrCustomerName, customerName) + tracer.SetAttribute(handle, schemas.AttrCustomerName, customerName) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostCustomerName, customerName) } if fallbackIndex, ok := ctx.Value(schemas.BifrostContextKeyFallbackIndex).(int); ok { - tracer.SetAttribute(handle, schemas.AttrFallbackIndex, fallbackIndex) + tracer.SetAttribute(handle, schemas.AttrFallbackIndex, fallbackIndex) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostFallbackIndex, fallbackIndex) } - tracer.SetAttribute(handle, schemas.AttrNumberOfRetries, attempts) + tracer.SetAttribute(handle, schemas.AttrNumberOfRetries, attempts) // legacy: gen_ai.* placement of bifrost-internal attr + tracer.SetAttribute(handle, schemas.AttrBifrostRetries, attempts) // Surface caller-supplied extra headers (from x-bf-eh-* and direct-allowlist // header forwarding) as span attributes so observability backends see the diff --git a/core/mcp/pluginpipeline.go b/core/mcp/pluginpipeline.go index 34b2e1b88d5..3032fcfd1f5 100644 --- a/core/mcp/pluginpipeline.go +++ b/core/mcp/pluginpipeline.go @@ -38,7 +38,7 @@ func (m *MCPManager) runWithPluginPipeline( ctx *schemas.BifrostContext, req *schemas.BifrostMCPRequest, op MCPOpFunc, -) (*schemas.BifrostMCPResponse, *schemas.BifrostError) { +) (finalResponse *schemas.BifrostMCPResponse, finalError *schemas.BifrostError) { // Ensure a request ID exists so plugin hooks have something to correlate on. // Connect/ping/list_tools fire from background contexts that typically lack one. if ctx != nil { @@ -57,9 +57,51 @@ func (m *MCPManager) runWithPluginPipeline( spanName = fmt.Sprintf("%s.%s", spanName, req.ClientName) } _, spanHandle = tracer.StartSpan(ctx, spanName, schemas.SpanKindMCPClient) + // Emit OTel GenAI tool-execution attributes on execute-tool spans so downstream + // backends can correlate tool calls with their requesting llm.call. + if req != nil && req.RequestType.IsExecuteTool() { + tracer.SetAttribute(spanHandle, schemas.AttrOperationName, schemas.OTelOperationNameExecuteTool) + tracer.SetAttribute(spanHandle, schemas.AttrToolType, "function") + if name := req.GetToolName(); name != "" { + tracer.SetAttribute(spanHandle, schemas.AttrToolName, name) + } + // GetToolArguments returns interface{}; the Responses branch boxes a + // *string, so a nil pointer survives the != nil guard. Unwrap and skip + // it explicitly, and deref non-nil so the attribute is the JSON string. + if args := req.GetToolArguments(); args != nil { + if p, ok := args.(*string); ok { + if p != nil { + tracer.SetAttribute(spanHandle, schemas.AttrToolCallArguments, *p) + } + } else { + tracer.SetAttribute(spanHandle, schemas.AttrToolCallArguments, args) + } + } + if req.ChatAssistantMessageToolCall != nil && req.ChatAssistantMessageToolCall.ID != nil { + tracer.SetAttribute(spanHandle, schemas.AttrToolCallID, *req.ChatAssistantMessageToolCall.ID) + } else if req.ResponsesToolMessage != nil && req.ResponsesToolMessage.CallID != nil { + tracer.SetAttribute(spanHandle, schemas.AttrToolCallID, *req.ResponsesToolMessage.CallID) + } + } } defer func() { - if tracer != nil { + if tracer == nil { + return + } + // Tool-call result captured via named returns — set just before EndSpan so the + // attribute lands on the open span before it's frozen. + if finalResponse != nil && req != nil && req.RequestType.IsExecuteTool() { + if data, err := schemas.MarshalString(finalResponse); err == nil { + tracer.SetAttribute(spanHandle, schemas.AttrToolCallResult, data) + } + } + if finalError != nil { + msg := "" + if finalError.Error != nil { + msg = finalError.Error.Message + } + tracer.EndSpan(spanHandle, schemas.SpanStatusError, msg) + } else { tracer.EndSpan(spanHandle, schemas.SpanStatusOk, "") } }() diff --git a/core/schemas/otelconv.go b/core/schemas/otelconv.go new file mode 100644 index 00000000000..c65090d53f3 --- /dev/null +++ b/core/schemas/otelconv.go @@ -0,0 +1,50 @@ +package schemas + +// OTelOperationNameExecuteTool is the gen_ai.operation.name value for MCP tool +// executions. execute_tool is an MCPRequestType, not a Bifrost RequestType, so it +// can't flow through OTelOperationName. +const OTelOperationNameExecuteTool = "execute_tool" + +// OTelOperationName maps a Bifrost RequestType to the value that should be +// emitted under gen_ai.operation.name. Values not modeled by the spec fall +// through to the raw RequestType string. +func OTelOperationName(rt RequestType) string { + switch rt { + case ChatCompletionRequest, ChatCompletionStreamRequest, + ResponsesRequest, ResponsesStreamRequest: + return "chat" + case TextCompletionRequest, TextCompletionStreamRequest: + return "text_completion" + case EmbeddingRequest: + return "embeddings" + case SpeechRequest, SpeechStreamRequest, + TranscriptionRequest, TranscriptionStreamRequest, + ImageGenerationRequest, ImageGenerationStreamRequest, + ImageEditRequest, ImageEditStreamRequest: + return "generate_content" + default: + return string(rt) + } +} + +// OTelProviderName maps a Bifrost ModelProvider to the value that should be +// emitted under gen_ai.provider.name. Providers not covered by the spec keep +// their Bifrost short name. +func OTelProviderName(p ModelProvider) string { + switch p { + case Bedrock: + return "aws.bedrock" + case Vertex: + return "gcp.vertex_ai" + case Gemini: + return "gcp.gemini" + case XAI: + return "x_ai" + case Mistral: + return "mistral_ai" + case Azure: + return "azure.ai.openai" + default: + return string(p) + } +} diff --git a/core/schemas/trace.go b/core/schemas/trace.go index 654a3d4f054..b65192f57ec 100644 --- a/core/schemas/trace.go +++ b/core/schemas/trace.go @@ -201,8 +201,9 @@ const ( // and are compatible with both OTEL and Datadog backends. const ( // Provider and Model Attributes - AttrProviderName = "gen_ai.provider.name" - AttrRequestModel = "gen_ai.request.model" + AttrProviderName = "gen_ai.provider.name" + AttrRequestModel = "gen_ai.request.model" + AttrOperationName = "gen_ai.operation.name" // Request Parameter Attributes AttrMaxTokens = "gen_ai.request.max_tokens" @@ -217,11 +218,16 @@ const ( AttrEcho = "gen_ai.request.echo" AttrLogitBias = "gen_ai.request.logit_bias" AttrLogProbs = "gen_ai.request.logprobs" - AttrN = "gen_ai.request.n" + AttrN = "gen_ai.request.n" // legacy: replaced by AttrChoiceCount + AttrChoiceCount = "gen_ai.request.choice.count" + // AttrEmbeddingsDimensionCount is the OTel spec key for embedding dimensions + // (Bifrost historically emitted AttrDimensions = gen_ai.request.dimensions). + AttrEmbeddingsDimensionCount = "gen_ai.embeddings.dimension.count" AttrSeed = "gen_ai.request.seed" AttrSuffix = "gen_ai.request.suffix" - AttrDimensions = "gen_ai.request.dimensions" - AttrEncodingFormat = "gen_ai.request.encoding_format" + AttrDimensions = "gen_ai.request.dimensions" // legacy: replaced by AttrEmbeddingsDimensionCount + AttrEncodingFormat = "gen_ai.request.encoding_format" // legacy: singular form; replaced by AttrEncodingFormats (string[]) + AttrEncodingFormats = "gen_ai.request.encoding_formats" AttrLanguage = "gen_ai.request.language" AttrPrompt = "gen_ai.request.prompt" AttrResponseFormat = "gen_ai.request.response_format" @@ -251,18 +257,26 @@ const ( AttrPluginErrorCount = "plugin.error_count" // Usage Attributes + // legacy: AttrPromptTokens / AttrCompletionTokens are the deprecated OTel names; + // new code should use AttrInputTokens / AttrOutputTokens. Kept for dashboards. AttrPromptTokens = "gen_ai.usage.prompt_tokens" AttrCompletionTokens = "gen_ai.usage.completion_tokens" AttrTotalTokens = "gen_ai.usage.total_tokens" AttrInputTokens = "gen_ai.usage.input_tokens" AttrOutputTokens = "gen_ai.usage.output_tokens" AttrUsageCost = "gen_ai.usage.cost" + // OTel GenAI spec keys for cache tokens (flat namespace). + AttrUsageCacheReadInputTokens = "gen_ai.usage.cache_read.input_tokens" + AttrUsageCacheCreationInputTokens = "gen_ai.usage.cache_creation.input_tokens" // Chat completion usage detail attributes + // legacy: nested namespace; OTel spec uses flat gen_ai.usage.cache_read.input_tokens + // and gen_ai.usage.cache_creation.input_tokens for the cached_* entries. The + // non-cached fields below have no spec equivalent and stay as-is. AttrPromptTokenDetailsText = "gen_ai.usage.prompt_token_details.text_tokens" AttrPromptTokenDetailsAudio = "gen_ai.usage.prompt_token_details.audio_tokens" AttrPromptTokenDetailsImage = "gen_ai.usage.prompt_token_details.image_tokens" - AttrPromptTokenDetailsCachedRead = "gen_ai.usage.prompt_token_details.cached_read_tokens" - AttrPromptTokenDetailsCachedWrite = "gen_ai.usage.prompt_token_details.cached_write_tokens" + AttrPromptTokenDetailsCachedRead = "gen_ai.usage.prompt_token_details.cached_read_tokens" // legacy: see AttrUsageCacheReadInputTokens + AttrPromptTokenDetailsCachedWrite = "gen_ai.usage.prompt_token_details.cached_write_tokens" // legacy: see AttrUsageCacheCreationInputTokens AttrPromptTokenDetailsCachedWrite5m = "gen_ai.usage.prompt_token_details.cached_write_tokens_5m" AttrPromptTokenDetailsCachedWrite1h = "gen_ai.usage.prompt_token_details.cached_write_tokens_1h" AttrCompletionTokenDetailsText = "gen_ai.usage.completion_token_details.text_tokens" @@ -275,7 +289,9 @@ const ( AttrCompletionTokenDetailsSearch = "gen_ai.usage.completion_token_details.num_search_queries" // Error Attributes - AttrError = "gen_ai.error" + AttrError = "gen_ai.error" + // legacy: AttrErrorType is the gen_ai.* placement; OTel general semconv uses the + // unprefixed "error.type". Emitted in parallel from PopulateErrorAttributes. AttrErrorType = "gen_ai.error.type" AttrErrorCode = "gen_ai.error.code" @@ -287,6 +303,9 @@ const ( AttrOutputMessages = "gen_ai.output.messages" // Bifrost Context Attributes + // legacy: every key below sits under gen_ai.* but represents a Bifrost-internal + // concept (governance / routing). The bifrost.* mirrors are the canonical home + // going forward; these will be dropped once dashboards migrate. AttrRequestID = "gen_ai.request_id" AttrVirtualKeyID = "gen_ai.virtual_key_id" AttrVirtualKeyName = "gen_ai.virtual_key_name" @@ -396,6 +415,50 @@ const ( AttrOutputTokenDetailsCite = "gen_ai.usage.output_token_details.citation_tokens" AttrOutputTokenDetailsSearch = "gen_ai.usage.output_token_details.num_search_queries" + // Tool execution attributes (OTel GenAI spec) used on MCP tool spans. + AttrToolName = "gen_ai.tool.name" + AttrToolCallID = "gen_ai.tool.call.id" + AttrToolCallArguments = "gen_ai.tool.call.arguments" + AttrToolCallResult = "gen_ai.tool.call.result" + AttrToolType = "gen_ai.tool.type" + + // ===================================================================== + // Bifrost-namespaced attributes (bifrost.*) + // + // Canonical home for everything that is NOT part of the OTel GenAI spec: + // - Bifrost-internal concepts (routing/governance, request id, retry counters) + // - Raw Bifrost short names that mirror canonicalized gen_ai.* values + // - Back-compat fallbacks for shape changes (e.g. comma-joined stop_sequences) + // + // The corresponding legacy gen_ai.* emissions are tagged "// legacy:" at their + // call sites and will be removed once dashboards migrate over. + // ===================================================================== + AttrBifrostProviderName = "bifrost.provider.name" + AttrBifrostRequestID = "bifrost.request.id" + AttrBifrostVirtualKeyID = "bifrost.virtual_key.id" + AttrBifrostVirtualKeyName = "bifrost.virtual_key.name" + AttrBifrostSelectedKeyID = "bifrost.selected_key.id" + AttrBifrostSelectedKeyName = "bifrost.selected_key.name" + AttrBifrostRoutingRuleID = "bifrost.routing_rule.id" + AttrBifrostRoutingRuleName = "bifrost.routing_rule.name" + AttrBifrostTeamID = "bifrost.team.id" + AttrBifrostTeamName = "bifrost.team.name" + AttrBifrostCustomerID = "bifrost.customer.id" + AttrBifrostCustomerName = "bifrost.customer.name" + AttrBifrostRetries = "bifrost.retries" + AttrBifrostFallbackIndex = "bifrost.fallback_index" + AttrBifrostStopSequencesJoined = "bifrost.request.stop_sequences" + + // OTel general semconv (no gen_ai prefix). Emitted alongside the legacy + // gen_ai.error.type from PopulateErrorAttributes. + AttrErrorTypeSpec = "error.type" + + // legacy: bare unprefixed keys retained for back-compat with existing dashboards. + // "request.type" is superseded by AttrOperationName; "retry.count" has no spec + // equivalent but stays under bifrost.retries going forward. + AttrLegacyRequestType = "request.type" + AttrLegacyRetryCount = "retry.count" + // File Operation Attributes AttrFileID = "gen_ai.file.id" AttrFileObject = "gen_ai.file.object" diff --git a/framework/tracing/llmspan.go b/framework/tracing/llmspan.go index a4c8e2cbb0f..72d013af488 100644 --- a/framework/tracing/llmspan.go +++ b/framework/tracing/llmspan.go @@ -41,8 +41,10 @@ func PopulateRequestAttributes(req *schemas.BifrostRequest) map[string]any { } provider, model, _ := req.GetRequestFields() - attrs[schemas.AttrProviderName] = string(provider) + attrs[schemas.AttrProviderName] = schemas.OTelProviderName(provider) + attrs[schemas.AttrBifrostProviderName] = string(provider) // raw Bifrost short name, mirrors canonical gen_ai.provider.name attrs[schemas.AttrRequestModel] = model + attrs[schemas.AttrOperationName] = schemas.OTelOperationName(req.RequestType) switch req.RequestType { case schemas.ChatCompletionRequest, schemas.ChatCompletionStreamRequest: @@ -137,7 +139,8 @@ func PopulateErrorAttributes(err *schemas.BifrostError) map[string]any { attrs[schemas.AttrError] = err.Error.Message if err.Error.Type != nil { - attrs[schemas.AttrErrorType] = *err.Error.Type + attrs[schemas.AttrErrorType] = *err.Error.Type // legacy: gen_ai.error.type; spec uses the unprefixed error.type + attrs[schemas.AttrErrorTypeSpec] = *err.Error.Type } if err.Error.Code != nil { attrs[schemas.AttrErrorCode] = *err.Error.Code @@ -156,28 +159,44 @@ func PopulateContextAttributes( customerID, customerName string, numberOfRetries, fallbackIndex int, ) { + // Each AttrXxx (gen_ai.*) emission below is LEGACY namespace pollution: a + // Bifrost-internal concept does not belong under gen_ai.*. The bifrost.* mirrors + // are the canonical home going forward; drop the gen_ai.* lines once dashboards + // migrate (grep for "// legacy:" inside this function). if virtualKeyID != "" { - attrs[schemas.AttrVirtualKeyID] = virtualKeyID - attrs[schemas.AttrVirtualKeyName] = virtualKeyName + attrs[schemas.AttrVirtualKeyID] = virtualKeyID // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrVirtualKeyName] = virtualKeyName // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrBifrostVirtualKeyID] = virtualKeyID + attrs[schemas.AttrBifrostVirtualKeyName] = virtualKeyName } if selectedKeyID != "" { - attrs[schemas.AttrSelectedKeyID] = selectedKeyID - attrs[schemas.AttrSelectedKeyName] = selectedKeyName + attrs[schemas.AttrSelectedKeyID] = selectedKeyID // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrSelectedKeyName] = selectedKeyName // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrBifrostSelectedKeyID] = selectedKeyID + attrs[schemas.AttrBifrostSelectedKeyName] = selectedKeyName } if routingRuleID != "" { - attrs[schemas.AttrRoutingRuleID] = routingRuleID - attrs[schemas.AttrRoutingRuleName] = routingRuleName + attrs[schemas.AttrRoutingRuleID] = routingRuleID // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrRoutingRuleName] = routingRuleName // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrBifrostRoutingRuleID] = routingRuleID + attrs[schemas.AttrBifrostRoutingRuleName] = routingRuleName } if teamID != "" { - attrs[schemas.AttrTeamID] = teamID - attrs[schemas.AttrTeamName] = teamName + attrs[schemas.AttrTeamID] = teamID // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrTeamName] = teamName // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrBifrostTeamID] = teamID + attrs[schemas.AttrBifrostTeamName] = teamName } if customerID != "" { - attrs[schemas.AttrCustomerID] = customerID - attrs[schemas.AttrCustomerName] = customerName - } - attrs[schemas.AttrNumberOfRetries] = numberOfRetries - attrs[schemas.AttrFallbackIndex] = fallbackIndex + attrs[schemas.AttrCustomerID] = customerID // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrCustomerName] = customerName // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrBifrostCustomerID] = customerID + attrs[schemas.AttrBifrostCustomerName] = customerName + } + attrs[schemas.AttrNumberOfRetries] = numberOfRetries // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrFallbackIndex] = fallbackIndex // legacy: gen_ai.* placement of bifrost-internal attr + attrs[schemas.AttrBifrostRetries] = numberOfRetries + attrs[schemas.AttrBifrostFallbackIndex] = fallbackIndex } // =============================================== @@ -201,7 +220,8 @@ func PopulateChatRequestAttributes(req *schemas.BifrostChatRequest, attrs map[st attrs[schemas.AttrTopP] = *req.Params.TopP } if req.Params.Stop != nil { - attrs[schemas.AttrStopSequences] = strings.Join(req.Params.Stop, ",") + attrs[schemas.AttrStopSequences] = append([]string(nil), req.Params.Stop...) + attrs[schemas.AttrBifrostStopSequencesJoined] = strings.Join(req.Params.Stop, ",") // legacy: comma-joined back-compat for dashboards predating the []string fix } if req.Params.PresencePenalty != nil { attrs[schemas.AttrPresencePenalty] = *req.Params.PresencePenalty @@ -273,9 +293,12 @@ func PopulateChatResponseAttributes(resp *schemas.BifrostChatResponse, attrs map // Usage if resp.Usage != nil { - attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens - attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens + attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.input_tokens + attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.output_tokens attrs[schemas.AttrTotalTokens] = resp.Usage.TotalTokens + // Spec keys. + attrs[schemas.AttrInputTokens] = resp.Usage.PromptTokens + attrs[schemas.AttrOutputTokens] = resp.Usage.CompletionTokens if resp.Usage.PromptTokensDetails != nil { if resp.Usage.PromptTokensDetails.TextTokens > 0 { @@ -288,10 +311,12 @@ func PopulateChatResponseAttributes(resp *schemas.BifrostChatResponse, attrs map attrs[schemas.AttrPromptTokenDetailsImage] = resp.Usage.PromptTokensDetails.ImageTokens } if resp.Usage.PromptTokensDetails.CachedReadTokens > 0 { - attrs[schemas.AttrPromptTokenDetailsCachedRead] = resp.Usage.PromptTokensDetails.CachedReadTokens + attrs[schemas.AttrPromptTokenDetailsCachedRead] = resp.Usage.PromptTokensDetails.CachedReadTokens // legacy: nested key; replaced by gen_ai.usage.cache_read.input_tokens + attrs[schemas.AttrUsageCacheReadInputTokens] = resp.Usage.PromptTokensDetails.CachedReadTokens } if resp.Usage.PromptTokensDetails.CachedWriteTokens > 0 { - attrs[schemas.AttrPromptTokenDetailsCachedWrite] = resp.Usage.PromptTokensDetails.CachedWriteTokens + attrs[schemas.AttrPromptTokenDetailsCachedWrite] = resp.Usage.PromptTokensDetails.CachedWriteTokens // legacy: nested key; replaced by gen_ai.usage.cache_creation.input_tokens + attrs[schemas.AttrUsageCacheCreationInputTokens] = resp.Usage.PromptTokensDetails.CachedWriteTokens } if d := resp.Usage.PromptTokensDetails.CachedWriteTokenDetails; d != nil { if d.CachedWriteTokens5m > 0 { @@ -357,7 +382,8 @@ func PopulateTextCompletionRequestAttributes(req *schemas.BifrostTextCompletionR attrs[schemas.AttrTopP] = *req.Params.TopP } if req.Params.Stop != nil { - attrs[schemas.AttrStopSequences] = strings.Join(req.Params.Stop, ",") + attrs[schemas.AttrStopSequences] = append([]string(nil), req.Params.Stop...) + attrs[schemas.AttrBifrostStopSequencesJoined] = strings.Join(req.Params.Stop, ",") // legacy: comma-joined back-compat for dashboards predating the []string fix } if req.Params.PresencePenalty != nil { attrs[schemas.AttrPresencePenalty] = *req.Params.PresencePenalty @@ -378,7 +404,8 @@ func PopulateTextCompletionRequestAttributes(req *schemas.BifrostTextCompletionR attrs[schemas.AttrLogProbs] = *req.Params.LogProbs } if req.Params.N != nil { - attrs[schemas.AttrN] = *req.Params.N + attrs[schemas.AttrN] = *req.Params.N // legacy: replaced by gen_ai.request.choice.count + attrs[schemas.AttrChoiceCount] = *req.Params.N } if req.Params.Seed != nil { attrs[schemas.AttrSeed] = *req.Params.Seed @@ -440,9 +467,12 @@ func PopulateTextCompletionResponseAttributes(resp *schemas.BifrostTextCompletio // Usage if resp.Usage != nil { - attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens - attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens + attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.input_tokens + attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.output_tokens attrs[schemas.AttrTotalTokens] = resp.Usage.TotalTokens + // Spec keys. + attrs[schemas.AttrInputTokens] = resp.Usage.PromptTokens + attrs[schemas.AttrOutputTokens] = resp.Usage.CompletionTokens } } @@ -458,10 +488,12 @@ func PopulateEmbeddingRequestAttributes(req *schemas.BifrostEmbeddingRequest, at if req.Params != nil { if req.Params.Dimensions != nil { - attrs[schemas.AttrDimensions] = *req.Params.Dimensions + attrs[schemas.AttrDimensions] = *req.Params.Dimensions // legacy: replaced by gen_ai.embeddings.dimension.count + attrs[schemas.AttrEmbeddingsDimensionCount] = *req.Params.Dimensions } if req.Params.EncodingFormat != nil { - attrs[schemas.AttrEncodingFormat] = *req.Params.EncodingFormat + attrs[schemas.AttrEncodingFormat] = *req.Params.EncodingFormat // legacy: singular form; replaced by gen_ai.request.encoding_formats (string[]) + attrs[schemas.AttrEncodingFormats] = []string{*req.Params.EncodingFormat} } // ExtraParams for k, v := range req.Params.ExtraParams { @@ -493,9 +525,12 @@ func PopulateEmbeddingResponseAttributes(resp *schemas.BifrostEmbeddingResponse, } // Usage if resp.Usage != nil { - attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens - attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens + attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.input_tokens + attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.output_tokens attrs[schemas.AttrTotalTokens] = resp.Usage.TotalTokens + // Spec keys. + attrs[schemas.AttrInputTokens] = resp.Usage.PromptTokens + attrs[schemas.AttrOutputTokens] = resp.Usage.CompletionTokens } } @@ -836,10 +871,12 @@ func PopulateResponsesResponseAttributes(resp *schemas.BifrostResponsesResponse, attrs[schemas.AttrInputTokenDetailsImage] = d.ImageTokens } if d.CachedReadTokens > 0 { - attrs[schemas.AttrInputTokenDetailsCachedRead] = d.CachedReadTokens + attrs[schemas.AttrInputTokenDetailsCachedRead] = d.CachedReadTokens // legacy: nested key; replaced by gen_ai.usage.cache_read.input_tokens + attrs[schemas.AttrUsageCacheReadInputTokens] = d.CachedReadTokens } if d.CachedWriteTokens > 0 { - attrs[schemas.AttrInputTokenDetailsCachedWrite] = d.CachedWriteTokens + attrs[schemas.AttrInputTokenDetailsCachedWrite] = d.CachedWriteTokens // legacy: nested key; replaced by gen_ai.usage.cache_creation.input_tokens + attrs[schemas.AttrUsageCacheCreationInputTokens] = d.CachedWriteTokens } if wd := d.CachedWriteTokenDetails; wd != nil { if wd.CachedWriteTokens5m > 0 { diff --git a/plugins/otel/converter.go b/plugins/otel/converter.go index 2875aee0fca..604dff5a105 100644 --- a/plugins/otel/converter.go +++ b/plugins/otel/converter.go @@ -147,7 +147,10 @@ func (p *OtelPlugin) convertTraceToResourceSpan(trace *schemas.Trace) *ResourceS } if span == trace.RootSpan { if requestID := trace.GetRequestID(); requestID != "" { - otelSpan.Attributes = append(otelSpan.Attributes, kvStr(schemas.AttrRequestID, requestID)) + otelSpan.Attributes = append(otelSpan.Attributes, + kvStr(schemas.AttrRequestID, requestID), // legacy: gen_ai.* placement of bifrost-internal attr; replaced by bifrost.request.id + kvStr(schemas.AttrBifrostRequestID, requestID), + ) } if len(p.instanceAttrs) > 0 { otelSpan.Attributes = append(otelSpan.Attributes, p.instanceAttrs...)