genai-otel-instrument 0.1.2.dev0__py3-none-any.whl → 0.1.7.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of genai-otel-instrument might be problematic. Click here for more details.

Files changed (24) hide show
  1. genai_otel/__version__.py +2 -2
  2. genai_otel/auto_instrument.py +18 -1
  3. genai_otel/config.py +22 -1
  4. genai_otel/cost_calculator.py +204 -13
  5. genai_otel/cost_enrichment_processor.py +175 -0
  6. genai_otel/gpu_metrics.py +50 -0
  7. genai_otel/instrumentors/base.py +300 -44
  8. genai_otel/instrumentors/cohere_instrumentor.py +140 -76
  9. genai_otel/instrumentors/huggingface_instrumentor.py +142 -13
  10. genai_otel/instrumentors/langchain_instrumentor.py +75 -75
  11. genai_otel/instrumentors/mistralai_instrumentor.py +234 -38
  12. genai_otel/instrumentors/ollama_instrumentor.py +104 -35
  13. genai_otel/instrumentors/replicate_instrumentor.py +59 -14
  14. genai_otel/instrumentors/togetherai_instrumentor.py +120 -16
  15. genai_otel/instrumentors/vertexai_instrumentor.py +79 -15
  16. genai_otel/llm_pricing.json +869 -589
  17. genai_otel/logging_config.py +45 -45
  18. genai_otel/py.typed +2 -2
  19. {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/METADATA +294 -33
  20. {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/RECORD +24 -23
  21. {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/WHEEL +0 -0
  22. {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/entry_points.txt +0 -0
  23. {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/licenses/LICENSE +0 -0
  24. {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/top_level.txt +0 -0
@@ -1,76 +1,140 @@
1
- """OpenTelemetry instrumentor for the Cohere SDK.
2
-
3
- This instrumentor automatically traces calls to Cohere models, capturing
4
- relevant attributes such as the model name.
5
- """
6
-
7
- import logging
8
- from typing import Dict, Optional
9
-
10
- from ..config import OTelConfig
11
- from .base import BaseInstrumentor
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- class CohereInstrumentor(BaseInstrumentor):
17
- """Instrumentor for Cohere"""
18
-
19
- def __init__(self):
20
- """Initialize the instrumentor."""
21
- super().__init__()
22
- self._cohere_available = False
23
- self._check_availability()
24
-
25
- def _check_availability(self):
26
- """Check if cohere library is available."""
27
- try:
28
- import cohere
29
-
30
- self._cohere_available = True
31
- logger.debug("cohere library detected and available for instrumentation")
32
- except ImportError:
33
- logger.debug("cohere library not installed, instrumentation will be skipped")
34
- self._cohere_available = False
35
-
36
- def instrument(self, config: OTelConfig):
37
- """Instrument cohere available if available."""
38
- if not self._cohere_available:
39
- logger.debug("Skipping instrumentation - library not available")
40
- return
41
-
42
- self.config = config
43
- try:
44
- import cohere
45
-
46
- original_init = cohere.Client.__init__
47
-
48
- def wrapped_init(instance, *args, **kwargs):
49
- original_init(instance, *args, **kwargs)
50
- self._instrument_client(instance)
51
-
52
- cohere.Client.__init__ = wrapped_init
53
-
54
- except ImportError:
55
- pass
56
-
57
- def _instrument_client(self, client):
58
- original_generate = client.generate
59
-
60
- def wrapped_generate(*args, **kwargs):
61
- with self.tracer.start_as_current_span("cohere.generate") as span:
62
- model = kwargs.get("model", "command")
63
-
64
- span.set_attribute("gen_ai.system", "cohere")
65
- span.set_attribute("gen_ai.request.model", model)
66
-
67
- if self.request_counter:
68
- self.request_counter.add(1, {"model": model, "provider": "cohere"})
69
-
70
- result = original_generate(*args, **kwargs)
71
- return result
72
-
73
- client.generate = wrapped_generate
74
-
75
- def _extract_usage(self, result) -> Optional[Dict[str, int]]:
76
- return None
1
+ """OpenTelemetry instrumentor for the Cohere SDK.
2
+
3
+ This instrumentor automatically traces calls to Cohere models, capturing
4
+ relevant attributes such as the model name and token usage.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, Optional
9
+
10
+ from ..config import OTelConfig
11
+ from .base import BaseInstrumentor
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CohereInstrumentor(BaseInstrumentor):
17
+ """Instrumentor for Cohere"""
18
+
19
+ def __init__(self):
20
+ """Initialize the instrumentor."""
21
+ super().__init__()
22
+ self._cohere_available = False
23
+ self._check_availability()
24
+
25
+ def _check_availability(self):
26
+ """Check if cohere library is available."""
27
+ try:
28
+ import cohere
29
+
30
+ self._cohere_available = True
31
+ logger.debug("cohere library detected and available for instrumentation")
32
+ except ImportError:
33
+ logger.debug("cohere library not installed, instrumentation will be skipped")
34
+ self._cohere_available = False
35
+
36
+ def instrument(self, config: OTelConfig):
37
+ """Instrument cohere if available."""
38
+ if not self._cohere_available:
39
+ logger.debug("Skipping instrumentation - library not available")
40
+ return
41
+
42
+ self.config = config
43
+ try:
44
+ import cohere
45
+
46
+ original_init = cohere.Client.__init__
47
+
48
+ def wrapped_init(instance, *args, **kwargs):
49
+ original_init(instance, *args, **kwargs)
50
+ self._instrument_client(instance)
51
+
52
+ cohere.Client.__init__ = wrapped_init
53
+ self._instrumented = True
54
+ logger.info("Cohere instrumentation enabled")
55
+
56
+ except Exception as e:
57
+ logger.error("Failed to instrument Cohere: %s", e, exc_info=True)
58
+ if config.fail_on_error:
59
+ raise
60
+
61
+ def _instrument_client(self, client):
62
+ """Instrument Cohere client methods."""
63
+ original_generate = client.generate
64
+
65
+ # Wrap using create_span_wrapper
66
+ wrapped_generate = self.create_span_wrapper(
67
+ span_name="cohere.generate",
68
+ extract_attributes=self._extract_generate_attributes,
69
+ )(original_generate)
70
+
71
+ client.generate = wrapped_generate
72
+
73
+ def _extract_generate_attributes(self, instance: Any, args: Any, kwargs: Any) -> Dict[str, Any]:
74
+ """Extract attributes from Cohere generate call.
75
+
76
+ Args:
77
+ instance: The client instance.
78
+ args: Positional arguments.
79
+ kwargs: Keyword arguments.
80
+
81
+ Returns:
82
+ Dict[str, Any]: Dictionary of attributes to set on the span.
83
+ """
84
+ attrs = {}
85
+ model = kwargs.get("model", "command")
86
+ prompt = kwargs.get("prompt", "")
87
+
88
+ attrs["gen_ai.system"] = "cohere"
89
+ attrs["gen_ai.request.model"] = model
90
+ attrs["gen_ai.operation.name"] = "generate"
91
+ attrs["gen_ai.request.message_count"] = 1 if prompt else 0
92
+
93
+ return attrs
94
+
95
+ def _extract_usage(self, result) -> Optional[Dict[str, int]]:
96
+ """Extract token usage from Cohere response.
97
+
98
+ Cohere responses include meta.tokens with:
99
+ - input_tokens: Input tokens
100
+ - output_tokens: Output tokens
101
+
102
+ Args:
103
+ result: The API response object.
104
+
105
+ Returns:
106
+ Optional[Dict[str, int]]: Dictionary with token counts or None.
107
+ """
108
+ try:
109
+ # Handle object response
110
+ if hasattr(result, "meta") and result.meta:
111
+ meta = result.meta
112
+ # Check for tokens object
113
+ if hasattr(meta, "tokens") and meta.tokens:
114
+ tokens = meta.tokens
115
+ input_tokens = getattr(tokens, "input_tokens", 0)
116
+ output_tokens = getattr(tokens, "output_tokens", 0)
117
+
118
+ if input_tokens or output_tokens:
119
+ return {
120
+ "prompt_tokens": int(input_tokens) if input_tokens else 0,
121
+ "completion_tokens": int(output_tokens) if output_tokens else 0,
122
+ "total_tokens": int(input_tokens or 0) + int(output_tokens or 0),
123
+ }
124
+ # Fallback to billed_units
125
+ elif hasattr(meta, "billed_units") and meta.billed_units:
126
+ billed = meta.billed_units
127
+ input_tokens = getattr(billed, "input_tokens", 0)
128
+ output_tokens = getattr(billed, "output_tokens", 0)
129
+
130
+ if input_tokens or output_tokens:
131
+ return {
132
+ "prompt_tokens": int(input_tokens) if input_tokens else 0,
133
+ "completion_tokens": int(output_tokens) if output_tokens else 0,
134
+ "total_tokens": int(input_tokens or 0) + int(output_tokens or 0),
135
+ }
136
+
137
+ return None
138
+ except Exception as e:
139
+ logger.debug("Failed to extract usage from Cohere response: %s", e)
140
+ return None
@@ -1,11 +1,14 @@
1
- """OpenTelemetry instrumentor for HuggingFace Transformers library.
1
+ """OpenTelemetry instrumentor for HuggingFace Transformers and Inference API.
2
2
 
3
- This instrumentor automatically traces calls made through HuggingFace pipelines,
4
- capturing relevant attributes such as the model name and task type.
3
+ This instrumentor automatically traces:
4
+ 1. HuggingFace Transformers pipelines (local model execution)
5
+ 2. HuggingFace Inference API calls via InferenceClient (used by smolagents)
6
+
7
+ Note: Transformers runs models locally (no API costs), but InferenceClient makes
8
+ API calls to HuggingFace endpoints which may have costs based on usage.
5
9
  """
6
10
 
7
11
  import logging
8
- import types
9
12
  from typing import Dict, Optional
10
13
 
11
14
  from ..config import OTelConfig
@@ -15,16 +18,22 @@ logger = logging.getLogger(__name__)
15
18
 
16
19
 
17
20
  class HuggingFaceInstrumentor(BaseInstrumentor):
18
- """Instrumentor for HuggingFace Transformers"""
21
+ """Instrumentor for HuggingFace Transformers and Inference API.
22
+
23
+ Instruments both:
24
+ - transformers.pipeline (local execution, no API costs)
25
+ - huggingface_hub.InferenceClient (API calls, may have costs)
26
+ """
19
27
 
20
28
  def __init__(self):
21
29
  """Initialize the instrumentor."""
22
30
  super().__init__()
23
31
  self._transformers_available = False
32
+ self._inference_client_available = False
24
33
  self._check_availability()
25
34
 
26
35
  def _check_availability(self):
27
- """Check if Transformers library is available."""
36
+ """Check if Transformers and InferenceClient libraries are available."""
28
37
  try:
29
38
  import transformers
30
39
 
@@ -34,12 +43,51 @@ class HuggingFaceInstrumentor(BaseInstrumentor):
34
43
  logger.debug("Transformers library not installed, instrumentation will be skipped")
35
44
  self._transformers_available = False
36
45
 
46
+ try:
47
+ from huggingface_hub import InferenceClient
48
+
49
+ self._inference_client_available = True
50
+ logger.debug("HuggingFace InferenceClient detected and available for instrumentation")
51
+ except ImportError:
52
+ logger.debug(
53
+ "huggingface_hub not installed, InferenceClient instrumentation will be skipped"
54
+ )
55
+ self._inference_client_available = False
56
+
37
57
  def instrument(self, config: OTelConfig):
58
+ """Instrument HuggingFace Transformers pipelines and InferenceClient."""
38
59
  self.config = config
39
60
 
40
- if not self._transformers_available:
41
- return
42
-
61
+ instrumented_count = 0
62
+
63
+ # Instrument transformers.pipeline if available
64
+ if self._transformers_available:
65
+ try:
66
+ self._instrument_transformers()
67
+ instrumented_count += 1
68
+ except Exception as e:
69
+ logger.error("Failed to instrument HuggingFace Transformers: %s", e, exc_info=True)
70
+ if config.fail_on_error:
71
+ raise
72
+
73
+ # Instrument InferenceClient if available
74
+ if self._inference_client_available:
75
+ try:
76
+ self._instrument_inference_client()
77
+ instrumented_count += 1
78
+ except Exception as e:
79
+ logger.error(
80
+ "Failed to instrument HuggingFace InferenceClient: %s", e, exc_info=True
81
+ )
82
+ if config.fail_on_error:
83
+ raise
84
+
85
+ if instrumented_count > 0:
86
+ self._instrumented = True
87
+ logger.info(f"HuggingFace instrumentation enabled ({instrumented_count} components)")
88
+
89
+ def _instrument_transformers(self):
90
+ """Instrument transformers.pipeline for local model execution."""
43
91
  try:
44
92
  import importlib
45
93
 
@@ -68,6 +116,7 @@ class HuggingFaceInstrumentor(BaseInstrumentor):
68
116
 
69
117
  span.set_attribute("gen_ai.system", "huggingface")
70
118
  span.set_attribute("gen_ai.request.model", model)
119
+ span.set_attribute("gen_ai.operation.name", task)
71
120
  span.set_attribute("huggingface.task", task)
72
121
 
73
122
  if instrumentor.request_counter:
@@ -88,10 +137,90 @@ class HuggingFaceInstrumentor(BaseInstrumentor):
88
137
  return WrappedPipeline(pipe)
89
138
 
90
139
  transformers_module.pipeline = wrapped_pipeline
91
- logger.info("HuggingFace instrumentation enabled")
92
-
93
- except ImportError:
94
- pass
140
+ logger.debug("HuggingFace Transformers pipeline instrumented")
141
+
142
+ except Exception as e:
143
+ raise # Re-raise to be caught by instrument() method
144
+
145
+ def _instrument_inference_client(self):
146
+ """Instrument HuggingFace InferenceClient for API calls."""
147
+ from huggingface_hub import InferenceClient
148
+
149
+ # Store original methods
150
+ original_chat_completion = InferenceClient.chat_completion
151
+ original_text_generation = InferenceClient.text_generation
152
+
153
+ # Wrap chat_completion method
154
+ wrapped_chat_completion = self.create_span_wrapper(
155
+ span_name="huggingface.inference.chat_completion",
156
+ extract_attributes=self._extract_inference_client_attributes,
157
+ )(original_chat_completion)
158
+
159
+ # Wrap text_generation method
160
+ wrapped_text_generation = self.create_span_wrapper(
161
+ span_name="huggingface.inference.text_generation",
162
+ extract_attributes=self._extract_inference_client_attributes,
163
+ )(original_text_generation)
164
+
165
+ InferenceClient.chat_completion = wrapped_chat_completion
166
+ InferenceClient.text_generation = wrapped_text_generation
167
+ logger.debug("HuggingFace InferenceClient instrumented")
168
+
169
+ def _extract_inference_client_attributes(self, instance, args, kwargs) -> Dict[str, str]:
170
+ """Extract attributes from Inference API call."""
171
+ attrs = {}
172
+ model = kwargs.get("model") or (args[0] if args else "unknown")
173
+
174
+ attrs["gen_ai.system"] = "huggingface"
175
+ attrs["gen_ai.request.model"] = str(model)
176
+ attrs["gen_ai.operation.name"] = "chat" # Default to chat
177
+
178
+ # Extract parameters if available
179
+ if "max_tokens" in kwargs:
180
+ attrs["gen_ai.request.max_tokens"] = kwargs["max_tokens"]
181
+ if "temperature" in kwargs:
182
+ attrs["gen_ai.request.temperature"] = kwargs["temperature"]
183
+ if "top_p" in kwargs:
184
+ attrs["gen_ai.request.top_p"] = kwargs["top_p"]
185
+
186
+ return attrs
95
187
 
96
188
  def _extract_usage(self, result) -> Optional[Dict[str, int]]:
189
+ """Extract token usage from HuggingFace response.
190
+
191
+ Handles both:
192
+ 1. Transformers pipeline (local execution) - returns None
193
+ 2. InferenceClient API calls - extracts token usage from response
194
+
195
+ Args:
196
+ result: The pipeline output or InferenceClient response.
197
+
198
+ Returns:
199
+ Dict with token counts for InferenceClient calls, None for local execution.
200
+ """
201
+ # Check if this is an InferenceClient API response
202
+ if result is not None and hasattr(result, "usage"):
203
+ usage = result.usage
204
+
205
+ # Extract token counts from usage object
206
+ prompt_tokens = getattr(usage, "prompt_tokens", None)
207
+ completion_tokens = getattr(usage, "completion_tokens", None)
208
+ total_tokens = getattr(usage, "total_tokens", None)
209
+
210
+ # If usage is a dict instead of object
211
+ if isinstance(usage, dict):
212
+ prompt_tokens = usage.get("prompt_tokens")
213
+ completion_tokens = usage.get("completion_tokens")
214
+ total_tokens = usage.get("total_tokens")
215
+
216
+ # Return token counts if available
217
+ if prompt_tokens is not None or completion_tokens is not None:
218
+ return {
219
+ "prompt_tokens": prompt_tokens or 0,
220
+ "completion_tokens": completion_tokens or 0,
221
+ "total_tokens": total_tokens or (prompt_tokens or 0) + (completion_tokens or 0),
222
+ }
223
+
224
+ # HuggingFace Transformers is free (local execution)
225
+ # No token-based costs to track
97
226
  return None
@@ -1,75 +1,75 @@
1
- """OpenTelemetry instrumentor for the LangChain framework.
2
-
3
- This instrumentor automatically traces various components within LangChain,
4
- including chains and agents, capturing relevant attributes for observability.
5
- """
6
-
7
- import logging
8
- from typing import Dict, Optional
9
-
10
- from ..config import OTelConfig
11
- from .base import BaseInstrumentor
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- class LangChainInstrumentor(BaseInstrumentor):
17
- """Instrumentor for LangChain"""
18
-
19
- def __init__(self):
20
- """Initialize the instrumentor."""
21
- super().__init__()
22
- self._langchain_available = False
23
- self._check_availability()
24
-
25
- def _check_availability(self):
26
- """Check if langchain library is available."""
27
- try:
28
- import langchain
29
-
30
- self._langchain_available = True
31
- logger.debug("langchain library detected and available for instrumentation")
32
- except ImportError:
33
- logger.debug("langchain library not installed, instrumentation will be skipped")
34
- self._langchain_available = False
35
-
36
- def instrument(self, config: OTelConfig):
37
- """Instrument langchain available if available."""
38
- if not self._langchain_available:
39
- logger.debug("Skipping instrumentation - library not available")
40
- return
41
-
42
- self.config = config
43
- try:
44
- from langchain.agents.agent import AgentExecutor
45
- from langchain.chains.base import Chain
46
-
47
- # Instrument Chains
48
- original_call = Chain.__call__
49
-
50
- def wrapped_call(instance, *args, **kwargs):
51
- chain_type = instance.__class__.__name__
52
- with self.tracer.start_as_current_span(f"langchain.chain.{chain_type}") as span:
53
- span.set_attribute("langchain.chain.type", chain_type)
54
- result = original_call(instance, *args, **kwargs)
55
- return result
56
-
57
- Chain.__call__ = wrapped_call
58
-
59
- # Instrument Agents
60
- original_agent_call = AgentExecutor.__call__
61
-
62
- def wrapped_agent_call(instance, *args, **kwargs):
63
- with self.tracer.start_as_current_span("langchain.agent.execute") as span:
64
- agent_name = getattr(instance, "agent", {}).get("name", "unknown")
65
- span.set_attribute("langchain.agent.name", agent_name)
66
- result = original_agent_call(instance, *args, **kwargs)
67
- return result
68
-
69
- AgentExecutor.__call__ = wrapped_agent_call
70
-
71
- except ImportError:
72
- pass
73
-
74
- def _extract_usage(self, result) -> Optional[Dict[str, int]]:
75
- return None
1
+ """OpenTelemetry instrumentor for the LangChain framework.
2
+
3
+ This instrumentor automatically traces various components within LangChain,
4
+ including chains and agents, capturing relevant attributes for observability.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, Optional
9
+
10
+ from ..config import OTelConfig
11
+ from .base import BaseInstrumentor
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class LangChainInstrumentor(BaseInstrumentor):
17
+ """Instrumentor for LangChain"""
18
+
19
+ def __init__(self):
20
+ """Initialize the instrumentor."""
21
+ super().__init__()
22
+ self._langchain_available = False
23
+ self._check_availability()
24
+
25
+ def _check_availability(self):
26
+ """Check if langchain library is available."""
27
+ try:
28
+ import langchain
29
+
30
+ self._langchain_available = True
31
+ logger.debug("langchain library detected and available for instrumentation")
32
+ except ImportError:
33
+ logger.debug("langchain library not installed, instrumentation will be skipped")
34
+ self._langchain_available = False
35
+
36
+ def instrument(self, config: OTelConfig):
37
+ """Instrument langchain available if available."""
38
+ if not self._langchain_available:
39
+ logger.debug("Skipping instrumentation - library not available")
40
+ return
41
+
42
+ self.config = config
43
+ try:
44
+ from langchain.agents.agent import AgentExecutor
45
+ from langchain.chains.base import Chain
46
+
47
+ # Instrument Chains
48
+ original_call = Chain.__call__
49
+
50
+ def wrapped_call(instance, *args, **kwargs):
51
+ chain_type = instance.__class__.__name__
52
+ with self.tracer.start_as_current_span(f"langchain.chain.{chain_type}") as span:
53
+ span.set_attribute("langchain.chain.type", chain_type)
54
+ result = original_call(instance, *args, **kwargs)
55
+ return result
56
+
57
+ Chain.__call__ = wrapped_call
58
+
59
+ # Instrument Agents
60
+ original_agent_call = AgentExecutor.__call__
61
+
62
+ def wrapped_agent_call(instance, *args, **kwargs):
63
+ with self.tracer.start_as_current_span("langchain.agent.execute") as span:
64
+ agent_name = getattr(instance, "agent", {}).get("name", "unknown")
65
+ span.set_attribute("langchain.agent.name", agent_name)
66
+ result = original_agent_call(instance, *args, **kwargs)
67
+ return result
68
+
69
+ AgentExecutor.__call__ = wrapped_agent_call
70
+
71
+ except ImportError:
72
+ pass
73
+
74
+ def _extract_usage(self, result) -> Optional[Dict[str, int]]:
75
+ return None