Skip to content

Commit ccfa056

Browse files
authored
feat(observability): add chat completion genai span attributes (#65)
1 parent 35eff35 commit ccfa056

23 files changed

Lines changed: 1601 additions & 98 deletions

File tree

src/config/entities/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ impl<T: Clone + 'static> ResourceStore<T> {
165165
deleted
166166
}
167167

168-
#[cfg(test)]
168+
/// Get an entry by its primary key
169169
fn get(&self, key: &str) -> Option<ResourceEntry<T>> {
170170
self.data.load().primary.get(key).cloned()
171171
}
@@ -295,16 +295,16 @@ impl<T: DeserializeOwned + Clone + Send + Sync + 'static> EntityStore<T> {
295295
Self { store }
296296
}
297297

298+
/// Get a snapshot of all entries
299+
pub fn list(&self) -> Arc<HashMap<String, ResourceEntry<T>>> {
300+
self.store.primary_snapshot()
301+
}
302+
298303
/// Get the value of the specified key
299-
#[cfg(test)]
300304
pub fn get(&self, key: &str) -> Option<ResourceEntry<T>> {
301305
self.store.get(key)
302306
}
303307

304-
pub fn list(&self) -> Arc<HashMap<String, ResourceEntry<T>>> {
305-
self.store.primary_snapshot()
306-
}
307-
308308
/// Get an entry via a secondary index
309309
fn get_by_secondary(&self, index: &str, sec_key: &str) -> Option<ResourceEntry<T>> {
310310
self.store.get_by_secondary(index, sec_key)

src/config/entities/models.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use utoipa::ToSchema;
88

99
use super::{ConfigProvider, EntityStore, IndexFn, ResourceEntry};
1010
use crate::{
11-
config::entities::types::{HasRateLimit, RateLimit, RateLimitMetric},
11+
config::entities::{
12+
Provider, ResourceRegistry,
13+
types::{HasRateLimit, RateLimit, RateLimitMetric},
14+
},
1215
utils::jsonschema::format_evaluation_error,
1316
};
1417

@@ -32,6 +35,13 @@ pub struct Model {
3235
pub rate_limit: Option<RateLimit>,
3336
}
3437

38+
impl Model {
39+
/// Get provider of current model
40+
pub fn provider(&self, resources: &ResourceRegistry) -> Option<ResourceEntry<Provider>> {
41+
resources.providers.get_by_id(&self.provider_id)
42+
}
43+
}
44+
3545
impl HasRateLimit for ResourceEntry<Model> {
3646
fn rate_limit(&self) -> Option<RateLimit> {
3747
self.rate_limit.clone()

src/config/entities/providers.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ impl ProvidersStore {
9898
pub fn list(&self) -> Arc<HashMap<String, ResourceEntry<Provider>>> {
9999
self.store.list()
100100
}
101+
102+
pub fn get_by_id(&self, id: &str) -> Option<ResourceEntry<Provider>> {
103+
self.store.get(id)
104+
}
101105
}
102106

103107
#[cfg(test)]

src/gateway/providers/azure.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::gateway::{
99
provider_instance::ProviderAuth,
1010
traits::{
1111
ChatTransform, CompatQuirks, EmbedTransform, ProviderCapabilities, ProviderMeta,
12-
provider::encode_path_segment,
12+
ProviderSemanticConventions, provider::encode_path_segment,
1313
},
1414
types::{
1515
embed::{EmbedRequestBody, EmbeddingRequest},
@@ -51,6 +51,14 @@ impl ProviderMeta for AzureDef {
5151
DEFAULT_BASE_URL
5252
}
5353

54+
fn semantic_conventions(&self) -> ProviderSemanticConventions {
55+
ProviderSemanticConventions {
56+
gen_ai_provider_name: "azure.ai.openai",
57+
llm_system: "openai",
58+
llm_provider: Some("azure"),
59+
}
60+
}
61+
5462
fn chat_endpoint_path(&self, model: &str) -> Cow<'static, str> {
5563
Cow::Owned(format!(
5664
"/openai/deployments/{}/chat/completions",
@@ -124,7 +132,10 @@ mod tests {
124132
use super::{AzureDef, DEFAULT_API_VERSION};
125133
use crate::gateway::{
126134
provider_instance::ProviderAuth,
127-
traits::{ChatTransform, EmbedTransform, ProviderCapabilities, ProviderMeta},
135+
traits::{
136+
ChatTransform, EmbedTransform, ProviderCapabilities, ProviderMeta,
137+
ProviderSemanticConventions,
138+
},
128139
types::{embed::EmbedRequestBody, openai::ChatCompletionRequest},
129140
};
130141

@@ -171,6 +182,14 @@ mod tests {
171182
);
172183
assert_eq!(embed_url.query(), Some("api-version=v1"));
173184
assert!(provider.as_embed_transform().is_some());
185+
assert_eq!(
186+
provider.semantic_conventions(),
187+
ProviderSemanticConventions {
188+
gen_ai_provider_name: "azure.ai.openai",
189+
llm_system: "openai",
190+
llm_provider: Some("azure"),
191+
}
192+
);
174193
}
175194

176195
#[test]

src/gateway/providers/bedrock.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::gateway::{
1717
provider_instance::ProviderAuth,
1818
traits::{
1919
ChatStreamState, ChatTransform, PreparedRequest, ProviderCapabilities, ProviderMeta,
20-
StreamReaderKind, provider::encode_path_segment,
20+
ProviderSemanticConventions, StreamReaderKind, provider::encode_path_segment,
2121
},
2222
types::openai::{ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse},
2323
};
@@ -77,6 +77,14 @@ impl ProviderMeta for BedrockDef {
7777
DEFAULT_BASE_URL
7878
}
7979

80+
fn semantic_conventions(&self) -> ProviderSemanticConventions {
81+
ProviderSemanticConventions {
82+
gen_ai_provider_name: "aws.bedrock",
83+
llm_system: "amazon",
84+
llm_provider: Some("aws"),
85+
}
86+
}
87+
8088
fn chat_endpoint_path(&self, model: &str) -> Cow<'static, str> {
8189
Cow::Owned(format!("/model/{}/converse", encode_path_segment(model)))
8290
}
@@ -217,7 +225,7 @@ mod tests {
217225
use super::{BedrockDef, BedrockProviderConfig};
218226
use crate::gateway::{
219227
provider_instance::ProviderAuth,
220-
traits::{PreparedRequest, ProviderMeta},
228+
traits::{PreparedRequest, ProviderMeta, ProviderSemanticConventions},
221229
};
222230

223231
#[test]
@@ -262,6 +270,15 @@ mod tests {
262270
fn build_url_uses_overlap_handling_and_encodes_model_ids_with_slashes() {
263271
let provider = BedrockDef;
264272

273+
assert_eq!(
274+
provider.semantic_conventions(),
275+
ProviderSemanticConventions {
276+
gen_ai_provider_name: "aws.bedrock",
277+
llm_system: "amazon",
278+
llm_provider: Some("aws"),
279+
}
280+
);
281+
265282
let url = provider.build_url(
266283
"https://bedrock-runtime.us-east-1.amazonaws.com/model",
267284
"inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0",

src/gateway/providers/gemini.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
66
use crate::gateway::{
77
error::{GatewayError, Result},
88
provider_instance::ProviderAuth,
9-
traits::{ChatTransform, EmbedTransform, ProviderCapabilities, ProviderMeta},
9+
traits::{
10+
ChatTransform, EmbedTransform, ProviderCapabilities, ProviderMeta,
11+
ProviderSemanticConventions,
12+
},
1013
};
1114

1215
pub const IDENTIFIER: &str = "gemini";
@@ -30,6 +33,14 @@ impl ProviderMeta for GoogleDef {
3033
"https://generativelanguage.googleapis.com/v1beta/openai"
3134
}
3235

36+
fn semantic_conventions(&self) -> ProviderSemanticConventions {
37+
ProviderSemanticConventions {
38+
gen_ai_provider_name: "gcp.gemini",
39+
llm_system: "gemini",
40+
llm_provider: Some("google"),
41+
}
42+
}
43+
3344
fn chat_endpoint_path(&self, _model: &str) -> Cow<'static, str> {
3445
Cow::Borrowed("/chat/completions")
3546
}
@@ -67,7 +78,7 @@ mod tests {
6778
use super::GoogleDef;
6879
use crate::gateway::{
6980
provider_instance::ProviderAuth,
70-
traits::{EmbedTransform, ProviderCapabilities, ProviderMeta},
81+
traits::{EmbedTransform, ProviderCapabilities, ProviderMeta, ProviderSemanticConventions},
7182
};
7283

7384
#[test]
@@ -92,5 +103,13 @@ mod tests {
92103
);
93104
assert_eq!(headers["x-goog-api-key"], "gemini-key");
94105
assert!(provider.as_embed_transform().is_some());
106+
assert_eq!(
107+
provider.semantic_conventions(),
108+
ProviderSemanticConventions {
109+
gen_ai_provider_name: "gcp.gemini",
110+
llm_system: "gemini",
111+
llm_provider: Some("google"),
112+
}
113+
);
95114
}
96115
}

src/gateway/traits/chat_format.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub trait ChatFormat: Send + Sync + 'static {
2626
type NativeStreamState: Default + Send + Unpin;
2727

2828
/// Stable format name used for logs and diagnostics.
29+
#[allow(unused)]
2930
fn name() -> &'static str;
3031

3132
/// Whether the request expects a streaming response.

src/gateway/traits/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub use native::{
1010
pub use native::{NativeOpenAIResponsesSupport, OpenAIResponsesNativeStreamState};
1111
pub use provider::{
1212
ChatTransform, CompatQuirks, EmbedTransform, PreparedRequest, ProviderCapabilities,
13-
ProviderMeta, StreamReaderKind,
13+
ProviderMeta, ProviderSemanticConventions, StreamReaderKind,
1414
};
1515
#[allow(unused_imports)]
1616
pub use provider::{ImageGenTransform, SttTransform, TtsTransform};

src/gateway/traits/provider.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,29 @@ pub(crate) fn encode_path_segment(segment: &str) -> String {
4545
utf8_percent_encode(segment, PATH_SEGMENT_ENCODE_SET).to_string()
4646
}
4747

48+
/// OpenTelemetry and OpenInference semantic conventions for the provider.
49+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50+
pub struct ProviderSemanticConventions {
51+
pub gen_ai_provider_name: &'static str,
52+
pub llm_system: &'static str,
53+
pub llm_provider: Option<&'static str>,
54+
}
55+
4856
/// Provider metadata with no data transformation logic.
4957
pub trait ProviderMeta: Send + Sync + 'static {
5058
fn name(&self) -> &'static str;
5159
fn default_base_url(&self) -> &'static str;
5260

61+
/// Get the provider's semantic conventions.
62+
/// Used for OpenTelemetry and OpenInference semantic conventions.
63+
fn semantic_conventions(&self) -> ProviderSemanticConventions {
64+
ProviderSemanticConventions {
65+
gen_ai_provider_name: self.name(),
66+
llm_system: self.name(),
67+
llm_provider: None,
68+
}
69+
}
70+
5371
/// Chat endpoint path for the provider. Implementations may use `model`
5472
/// for providers whose route shape depends on the model name.
5573
fn chat_endpoint_path(&self, _model: &str) -> Cow<'static, str> {
@@ -306,7 +324,10 @@ mod tests {
306324
use http::HeaderMap;
307325
use serde_json::json;
308326

309-
use super::{ChatTransform, CompatQuirks, EmbedTransform, ProviderMeta, StreamReaderKind};
327+
use super::{
328+
ChatTransform, CompatQuirks, EmbedTransform, ProviderMeta, ProviderSemanticConventions,
329+
StreamReaderKind,
330+
};
310331
use crate::gateway::{
311332
provider_instance::ProviderAuth,
312333
traits::chat_format::ChatStreamState,
@@ -421,6 +442,20 @@ mod tests {
421442
assert_eq!(body["stream_options"]["include_usage"], true);
422443
}
423444

445+
#[test]
446+
fn provider_meta_uses_name_based_default_semantic_conventions() {
447+
let provider = DummyProvider;
448+
449+
assert_eq!(
450+
provider.semantic_conventions(),
451+
ProviderSemanticConventions {
452+
gen_ai_provider_name: "dummy",
453+
llm_system: "dummy",
454+
llm_provider: None,
455+
}
456+
);
457+
}
458+
424459
#[test]
425460
fn apply_to_request_skips_stream_usage_for_non_streaming_requests() {
426461
let quirks = CompatQuirks {

0 commit comments

Comments
 (0)