openlit 1.34.18__py3-none-any.whl → 1.34.20__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.
@@ -3,63 +3,92 @@ Anthropic OpenTelemetry instrumentation utility functions
3
3
  """
4
4
  import time
5
5
 
6
- from opentelemetry.sdk.resources import SERVICE_NAME, TELEMETRY_SDK_NAME, DEPLOYMENT_ENVIRONMENT
7
6
  from opentelemetry.trace import Status, StatusCode
8
7
 
9
8
  from openlit.__helpers import (
10
9
  calculate_ttft,
11
10
  response_as_dict,
12
11
  calculate_tbt,
13
- extract_and_format_input,
14
12
  get_chat_model_cost,
15
- create_metrics_attributes,
16
- otel_event,
17
- concatenate_all_contents
13
+ record_completion_metrics,
14
+ common_span_attributes,
18
15
  )
19
16
  from openlit.semcov import SemanticConvention
20
17
 
21
- def process_chunk(self, chunk):
18
+ def format_content(messages):
19
+ """
20
+ Format the messages into a string for span events.
21
+ """
22
+
23
+ if not messages:
24
+ return ""
25
+
26
+ formatted_messages = []
27
+ for message in messages:
28
+ if isinstance(message, dict):
29
+ role = message.get("role", "user")
30
+ content = message.get("content", "")
31
+ else:
32
+ # Handle Anthropic object format
33
+ role = getattr(message, "role", "user")
34
+ content = getattr(message, "content", "")
35
+
36
+ if isinstance(content, list):
37
+ # Handle structured content (e.g., text + images)
38
+ text_parts = []
39
+ for part in content:
40
+ if isinstance(part, dict) and part.get("type") == "text":
41
+ text_parts.append(part.get("text", ""))
42
+ content = " ".join(text_parts)
43
+ elif not isinstance(content, str):
44
+ content = str(content)
45
+
46
+ formatted_messages.append(f"{role}: {content}")
47
+
48
+ return "\n".join(formatted_messages)
49
+
50
+ def process_chunk(scope, chunk):
22
51
  """
23
52
  Process a chunk of response data and update state.
24
53
  """
25
54
 
26
55
  end_time = time.time()
27
56
  # Record the timestamp for the current chunk
28
- self._timestamps.append(end_time)
57
+ scope._timestamps.append(end_time)
29
58
 
30
- if len(self._timestamps) == 1:
59
+ if len(scope._timestamps) == 1:
31
60
  # Calculate time to first chunk
32
- self._ttft = calculate_ttft(self._timestamps, self._start_time)
61
+ scope._ttft = calculate_ttft(scope._timestamps, scope._start_time)
33
62
 
34
63
  chunked = response_as_dict(chunk)
35
64
 
36
65
  # Collect message IDs and input token from events
37
- if chunked.get('type') == 'message_start':
38
- self._response_id = chunked.get('message').get('id')
39
- self._input_tokens = chunked.get('message').get('usage').get('input_tokens')
40
- self._response_model = chunked.get('message').get('model')
41
- self._response_role = chunked.get('message').get('role')
66
+ if chunked.get("type") == "message_start":
67
+ scope._response_id = chunked.get("message").get("id")
68
+ scope._input_tokens = chunked.get("message").get("usage").get("input_tokens")
69
+ scope._response_model = chunked.get("message").get("model")
70
+ scope._response_role = chunked.get("message").get("role")
42
71
 
43
72
  # Collect message IDs and aggregated response from events
44
- if chunked.get('type') == 'content_block_delta':
45
- if chunked.get('delta').get('text'):
46
- self._llmresponse += chunked.get('delta').get('text')
47
- elif chunked.get('delta').get('partial_json'):
48
- self._tool_arguments += chunked.get('delta').get('partial_json')
49
-
50
- if chunked.get('type') == 'content_block_start':
51
- if chunked.get('content_block').get('id'):
52
- self._tool_id = chunked.get('content_block').get('id')
53
- if chunked.get('content_block').get('name'):
54
- self._tool_name = chunked.get('content_block').get('name')
73
+ if chunked.get("type") == "content_block_delta":
74
+ if chunked.get("delta").get("text"):
75
+ scope._llmresponse += chunked.get("delta").get("text")
76
+ elif chunked.get("delta").get("partial_json"):
77
+ scope._tool_arguments += chunked.get("delta").get("partial_json")
78
+
79
+ if chunked.get("type") == "content_block_start":
80
+ if chunked.get("content_block").get("id"):
81
+ scope._tool_id = chunked.get("content_block").get("id")
82
+ if chunked.get("content_block").get("name"):
83
+ scope._tool_name = chunked.get("content_block").get("name")
55
84
 
56
85
  # Collect output tokens and stop reason from events
57
- if chunked.get('type') == 'message_delta':
58
- self._output_tokens = chunked.get('usage').get('output_tokens')
59
- self._finish_reason = chunked.get('delta').get('stop_reason')
86
+ if chunked.get("type") == "message_delta":
87
+ scope._output_tokens = chunked.get("usage").get("output_tokens")
88
+ scope._finish_reason = chunked.get("delta").get("stop_reason")
60
89
 
61
90
  def common_chat_logic(scope, pricing_info, environment, application_name, metrics,
62
- event_provider, capture_message_content, disable_metrics, version, is_stream):
91
+ capture_message_content, disable_metrics, version, is_stream):
63
92
  """
64
93
  Process chat request and generate Telemetry
65
94
  """
@@ -68,48 +97,56 @@ def common_chat_logic(scope, pricing_info, environment, application_name, metric
68
97
  if len(scope._timestamps) > 1:
69
98
  scope._tbt = calculate_tbt(scope._timestamps)
70
99
 
71
- formatted_messages = extract_and_format_input(scope._kwargs.get('messages', ''))
72
- request_model = scope._kwargs.get('model', 'claude-3-opus-20240229')
100
+ formatted_messages = format_content(scope._kwargs.get("messages", []))
101
+ request_model = scope._kwargs.get("model", "claude-3-5-sonnet-latest")
73
102
 
74
103
  cost = get_chat_model_cost(request_model, pricing_info, scope._input_tokens, scope._output_tokens)
75
104
 
76
- # Set Span attributes (OTel Semconv)
77
- scope._span.set_attribute(TELEMETRY_SDK_NAME, 'openlit')
78
- scope._span.set_attribute(SemanticConvention.GEN_AI_OPERATION, SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT)
79
- scope._span.set_attribute(SemanticConvention.GEN_AI_SYSTEM, SemanticConvention.GEN_AI_SYSTEM_ANTHROPIC)
80
- scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MODEL, request_model)
81
- scope._span.set_attribute(SemanticConvention.SERVER_PORT, scope._server_port)
82
- scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MAX_TOKENS, scope._kwargs.get('max_tokens', -1))
83
- scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_STOP_SEQUENCES, scope._kwargs.get('stop_sequences', []))
84
- scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TEMPERATURE, scope._kwargs.get('temperature', 1.0))
85
- scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TOP_K, scope._kwargs.get('top_k', 1.0))
86
- scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TOP_P, scope._kwargs.get('top_p', 1.0))
87
- scope._span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_FINISH_REASON, [scope._finish_reason])
105
+ # Common Span Attributes
106
+ common_span_attributes(scope,
107
+ SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT, SemanticConvention.GEN_AI_SYSTEM_ANTHROPIC,
108
+ scope._server_address, scope._server_port, request_model, scope._response_model,
109
+ environment, application_name, is_stream, scope._tbt, scope._ttft, version)
110
+
111
+ # Span Attributes for Request parameters
112
+ scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MAX_TOKENS, scope._kwargs.get("max_tokens", -1))
113
+ scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_STOP_SEQUENCES, scope._kwargs.get("stop_sequences", []))
114
+ scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TEMPERATURE, scope._kwargs.get("temperature", 1.0))
115
+ scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TOP_K, scope._kwargs.get("top_k", 1.0))
116
+ scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TOP_P, scope._kwargs.get("top_p", 1.0))
117
+
118
+ # Span Attributes for Response parameters
88
119
  scope._span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_ID, scope._response_id)
89
- scope._span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_MODEL, scope._response_model)
120
+ scope._span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_FINISH_REASON, [scope._finish_reason])
121
+ scope._span.set_attribute(SemanticConvention.GEN_AI_OUTPUT_TYPE, "text" if isinstance(scope._llmresponse, str) else "json")
122
+
123
+ # Span Attributes for Cost and Tokens
90
124
  scope._span.set_attribute(SemanticConvention.GEN_AI_USAGE_INPUT_TOKENS, scope._input_tokens)
91
125
  scope._span.set_attribute(SemanticConvention.GEN_AI_USAGE_OUTPUT_TOKENS, scope._output_tokens)
92
- scope._span.set_attribute(SemanticConvention.SERVER_ADDRESS, scope._server_address)
93
-
94
- scope._span.set_attribute(SemanticConvention.GEN_AI_OUTPUT_TYPE,
95
- 'text' if isinstance(scope._llmresponse, str) else 'json')
96
-
97
- scope._span.set_attribute(DEPLOYMENT_ENVIRONMENT, environment)
98
- scope._span.set_attribute(SERVICE_NAME, application_name)
99
- scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_IS_STREAM, is_stream)
100
126
  scope._span.set_attribute(SemanticConvention.GEN_AI_CLIENT_TOKEN_USAGE, scope._input_tokens + scope._output_tokens)
101
127
  scope._span.set_attribute(SemanticConvention.GEN_AI_USAGE_COST, cost)
102
- scope._span.set_attribute(SemanticConvention.GEN_AI_SERVER_TBT, scope._tbt)
103
- scope._span.set_attribute(SemanticConvention.GEN_AI_SERVER_TTFT, scope._ttft)
104
- scope._span.set_attribute(SemanticConvention.GEN_AI_SDK_VERSION, version)
105
128
 
106
- # To be removed one the change to log events (from span events) is complete
107
- prompt = concatenate_all_contents(formatted_messages)
129
+ # Handle tool calls if present
130
+ if scope._tool_calls:
131
+ # Optimized tool handling - extract name, id, and arguments
132
+ tool_name = scope._tool_calls.get("name", "")
133
+ tool_id = scope._tool_calls.get("id", "")
134
+ tool_args = scope._tool_calls.get("input", "")
135
+
136
+ scope._span.set_attribute(SemanticConvention.GEN_AI_TOOL_NAME, tool_name)
137
+ scope._span.set_attribute(SemanticConvention.GEN_AI_TOOL_CALL_ID, tool_id)
138
+ scope._span.set_attribute(SemanticConvention.GEN_AI_TOOL_ARGS, str(tool_args))
139
+
140
+ # Span Attributes for Content
108
141
  if capture_message_content:
142
+ scope._span.set_attribute(SemanticConvention.GEN_AI_CONTENT_PROMPT, formatted_messages)
143
+ scope._span.set_attribute(SemanticConvention.GEN_AI_CONTENT_COMPLETION, scope._llmresponse)
144
+
145
+ # To be removed once the change to span_attributes (from span events) is complete
109
146
  scope._span.add_event(
110
147
  name=SemanticConvention.GEN_AI_CONTENT_PROMPT_EVENT,
111
148
  attributes={
112
- SemanticConvention.GEN_AI_CONTENT_PROMPT: prompt,
149
+ SemanticConvention.GEN_AI_CONTENT_PROMPT: formatted_messages,
113
150
  },
114
151
  )
115
152
  scope._span.add_event(
@@ -119,133 +156,70 @@ def common_chat_logic(scope, pricing_info, environment, application_name, metric
119
156
  },
120
157
  )
121
158
 
122
- choice_event_body = {
123
- 'finish_reason': scope._finish_reason,
124
- 'index': 0,
125
- 'message': {
126
- **({'content': scope._llmresponse} if capture_message_content else {}),
127
- 'role': scope._response_role
128
- }
129
- }
130
-
131
- if scope._tool_calls:
132
- choice_event_body['message'].update({
133
- 'tool_calls': {
134
- 'function': {
135
- 'name': scope._tool_calls.get('name', ''),
136
- 'arguments': scope._tool_calls.get('input', '')
137
- },
138
- 'id': scope._tool_calls.get('id', ''),
139
- 'type': 'function'
140
- }
141
- })
142
-
143
- # Emit events
144
- for role in ['user', 'system', 'assistant', 'tool']:
145
- if formatted_messages.get(role, {}).get('content', ''):
146
- event = otel_event(
147
- name=getattr(SemanticConvention, f'GEN_AI_{role.upper()}_MESSAGE'),
148
- attributes={
149
- SemanticConvention.GEN_AI_SYSTEM: SemanticConvention.GEN_AI_SYSTEM_ANTHROPIC
150
- },
151
- body = {
152
- # pylint: disable=line-too-long
153
- **({'content': formatted_messages.get(role, {}).get('content', '')} if capture_message_content else {}),
154
- 'role': formatted_messages.get(role, {}).get('role', []),
155
- **({
156
- 'tool_calls': {
157
- 'function': {
158
- # pylint: disable=line-too-long
159
- 'name': (scope._tool_calls[0].get('function', {}).get('name', '') if scope._tool_calls else ''),
160
- 'arguments': (scope._tool_calls[0].get('function', {}).get('arguments', '') if scope._tool_calls else '')
161
- },
162
- 'id': (scope._tool_calls[0].get('id', '') if scope._tool_calls else ''),
163
- 'type': 'function'
164
- }
165
- } if role == 'assistant' else {}),
166
- **({
167
- 'id': (scope._tool_calls[0].get('id', '') if scope._tool_calls else '')
168
- } if role == 'tool' else {})
169
- }
170
- )
171
- event_provider.emit(event)
172
-
173
- choice_event = otel_event(
174
- name=SemanticConvention.GEN_AI_CHOICE,
175
- attributes={
176
- SemanticConvention.GEN_AI_SYSTEM: SemanticConvention.GEN_AI_SYSTEM_ANTHROPIC
177
- },
178
- body=choice_event_body
179
- )
180
- event_provider.emit(choice_event)
181
-
182
159
  scope._span.set_status(Status(StatusCode.OK))
183
160
 
161
+ # Record metrics
184
162
  if not disable_metrics:
185
- metrics_attributes = create_metrics_attributes(
186
- service_name=application_name,
187
- deployment_environment=environment,
188
- operation=SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT,
189
- system=SemanticConvention.GEN_AI_SYSTEM_ANTHROPIC,
190
- request_model=request_model,
191
- server_address=scope._server_address,
192
- server_port=scope._server_port,
193
- response_model=scope._response_model,
194
- )
163
+ record_completion_metrics(metrics, SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT, SemanticConvention.GEN_AI_SYSTEM_ANTHROPIC,
164
+ scope._server_address, scope._server_port, request_model, scope._response_model, environment,
165
+ application_name, scope._start_time, scope._end_time, scope._input_tokens, scope._output_tokens,
166
+ cost, scope._tbt, scope._ttft)
195
167
 
196
- metrics['genai_client_usage_tokens'].record(scope._input_tokens + scope._output_tokens, metrics_attributes)
197
- metrics['genai_client_operation_duration'].record(scope._end_time - scope._start_time, metrics_attributes)
198
- metrics['genai_server_tbt'].record(scope._tbt, metrics_attributes)
199
- metrics['genai_server_ttft'].record(scope._ttft, metrics_attributes)
200
- metrics['genai_requests'].add(1, metrics_attributes)
201
- metrics['genai_completion_tokens'].add(scope._output_tokens, metrics_attributes)
202
- metrics['genai_prompt_tokens'].add(scope._input_tokens, metrics_attributes)
203
- metrics['genai_cost'].record(cost, metrics_attributes)
204
-
205
- def process_streaming_chat_response(self, pricing_info, environment, application_name, metrics,
206
- event_provider, capture_message_content=False, disable_metrics=False, version=''):
168
+ def process_streaming_chat_response(scope, pricing_info, environment, application_name, metrics,
169
+ capture_message_content=False, disable_metrics=False, version=""):
207
170
  """
208
- Process chat request and generate Telemetry
171
+ Process streaming chat response and generate telemetry.
209
172
  """
210
- if self._tool_id != '':
211
- self._tool_calls = {
212
- 'id': self._tool_id,
213
- 'name': self._tool_name,
214
- 'input': self._tool_arguments
173
+
174
+ if scope._tool_id != "":
175
+ scope._tool_calls = {
176
+ "id": scope._tool_id,
177
+ "name": scope._tool_name,
178
+ "input": scope._tool_arguments
215
179
  }
216
180
 
217
- common_chat_logic(self, pricing_info, environment, application_name, metrics,
218
- event_provider, capture_message_content, disable_metrics, version, is_stream=True)
181
+ common_chat_logic(scope, pricing_info, environment, application_name, metrics,
182
+ capture_message_content, disable_metrics, version, is_stream=True)
219
183
 
220
184
  def process_chat_response(response, request_model, pricing_info, server_port, server_address,
221
- environment, application_name, metrics, event_provider, start_time,
222
- span, capture_message_content=False, disable_metrics=False, version='1.0.0', **kwargs):
185
+ environment, application_name, metrics, start_time,
186
+ span, capture_message_content=False, disable_metrics=False, version="1.0.0", **kwargs):
223
187
  """
224
- Process chat request and generate Telemetry
188
+ Process non-streaming chat response and generate telemetry.
225
189
  """
226
190
 
227
- self = type('GenericScope', (), {})()
191
+ scope = type("GenericScope", (), {})()
228
192
  response_dict = response_as_dict(response)
229
193
 
230
194
  # pylint: disable = no-member
231
- self._start_time = start_time
232
- self._end_time = time.time()
233
- self._span = span
234
- self._llmresponse = response_dict.get('content', {})[0].get('text', '')
235
- self._response_role = response_dict.get('message', {}).get('role', 'assistant')
236
- self._input_tokens = response_dict.get('usage').get('input_tokens')
237
- self._output_tokens = response_dict.get('usage').get('output_tokens')
238
- self._response_model = response_dict.get('model', '')
239
- self._finish_reason = response_dict.get('stop_reason', '')
240
- self._response_id = response_dict.get('id', '')
241
- self._timestamps = []
242
- self._ttft, self._tbt = self._end_time - self._start_time, 0
243
- self._server_address, self._server_port = server_address, server_port
244
- self._kwargs = kwargs
245
- #pylint: disable=line-too-long
246
- self._tool_calls = (lambda c: c[1] if len(c) > 1 and c[1].get('type') == 'tool_use' else None)(response_dict.get('content', []))
247
-
248
- common_chat_logic(self, pricing_info, environment, application_name, metrics,
249
- event_provider, capture_message_content, disable_metrics, version, is_stream=False)
195
+ scope._start_time = start_time
196
+ scope._end_time = time.time()
197
+ scope._span = span
198
+ scope._llmresponse = response_dict.get("content", [{}])[0].get("text", "")
199
+ scope._response_role = response_dict.get("role", "assistant")
200
+ scope._input_tokens = response_dict.get("usage").get("input_tokens")
201
+ scope._output_tokens = response_dict.get("usage").get("output_tokens")
202
+ scope._response_model = response_dict.get("model", "")
203
+ scope._finish_reason = response_dict.get("stop_reason", "")
204
+ scope._response_id = response_dict.get("id", "")
205
+ scope._timestamps = []
206
+ scope._ttft, scope._tbt = scope._end_time - scope._start_time, 0
207
+ scope._server_address, scope._server_port = server_address, server_port
208
+ scope._kwargs = kwargs
209
+
210
+ # Handle tool calls if present
211
+ content_blocks = response_dict.get("content", [])
212
+ scope._tool_calls = None
213
+ for block in content_blocks:
214
+ if block.get("type") == "tool_use":
215
+ scope._tool_calls = {
216
+ "id": block.get("id", ""),
217
+ "name": block.get("name", ""),
218
+ "input": block.get("input", "")
219
+ }
220
+ break
221
+
222
+ common_chat_logic(scope, pricing_info, environment, application_name, metrics,
223
+ capture_message_content, disable_metrics, version, is_stream=False)
250
224
 
251
225
  return response
@@ -1,4 +1,3 @@
1
- # pylint: disable=useless-return, bad-staticmethod-argument, disable=duplicate-code
2
1
  """Initializer of Auto Instrumentation of AWS Bedrock Functions"""
3
2
 
4
3
  from typing import Collection
@@ -6,37 +5,43 @@ import importlib.metadata
6
5
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
7
6
  from wrapt import wrap_function_wrapper
8
7
 
9
- from openlit.instrumentation.bedrock.bedrock import converse
8
+ from openlit.instrumentation.bedrock.bedrock import converse, converse_stream
10
9
 
11
10
  _instruments = ("boto3 >= 1.34.138",)
12
11
 
13
12
  class BedrockInstrumentor(BaseInstrumentor):
14
13
  """
15
- An instrumentor for AWS Bedrock's client library.
14
+ An instrumentor for AWS Bedrock client library.
16
15
  """
17
16
 
18
17
  def instrumentation_dependencies(self) -> Collection[str]:
19
18
  return _instruments
20
19
 
21
20
  def _instrument(self, **kwargs):
22
- application_name = kwargs.get("application_name", "default_application")
23
- environment = kwargs.get("environment", "default_environment")
21
+ version = importlib.metadata.version("boto3")
22
+ environment = kwargs.get("environment", "default")
23
+ application_name = kwargs.get("application_name", "default")
24
24
  tracer = kwargs.get("tracer")
25
- event_provider = kwargs.get('event_provider')
26
- metrics = kwargs.get("metrics_dict")
27
25
  pricing_info = kwargs.get("pricing_info", {})
28
26
  capture_message_content = kwargs.get("capture_message_content", False)
27
+ metrics = kwargs.get("metrics_dict")
29
28
  disable_metrics = kwargs.get("disable_metrics")
30
- version = importlib.metadata.version("boto3")
31
29
 
32
- #sync
30
+ # sync
31
+ wrap_function_wrapper(
32
+ "botocore.client",
33
+ "ClientCreator.create_client",
34
+ converse(version, environment, application_name, tracer, pricing_info,
35
+ capture_message_content, metrics, disable_metrics),
36
+ )
37
+
38
+ # streaming
33
39
  wrap_function_wrapper(
34
- "botocore.client",
35
- "ClientCreator.create_client",
36
- converse(version, environment, application_name,
37
- tracer, event_provider, pricing_info, capture_message_content, metrics, disable_metrics),
40
+ "botocore.client",
41
+ "ClientCreator.create_client",
42
+ converse_stream(version, environment, application_name, tracer, pricing_info,
43
+ capture_message_content, metrics, disable_metrics),
38
44
  )
39
45
 
40
46
  def _uninstrument(self, **kwargs):
41
- # Proper uninstrumentation logic to revert patched methods
42
47
  pass