revenium-python-sdk 0.1.0__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.
- revenium_middleware/__init__.py +184 -0
- revenium_middleware/_core/__init__.py +65 -0
- revenium_middleware/_core/config.py +165 -0
- revenium_middleware/_core/context.py +109 -0
- revenium_middleware/_core/decorators.py +202 -0
- revenium_middleware/_core/metering.py +207 -0
- revenium_middleware/_core/prompt_extraction.py +55 -0
- revenium_middleware/_core/subscriber.py +51 -0
- revenium_middleware/_core/trace_fields.py +265 -0
- revenium_middleware/anthropic/__init__.py +108 -0
- revenium_middleware/anthropic/bedrock_adapter.py +753 -0
- revenium_middleware/anthropic/config.py +29 -0
- revenium_middleware/anthropic/middleware.py +1070 -0
- revenium_middleware/anthropic/prompt_extractor.py +178 -0
- revenium_middleware/anthropic/provider.py +141 -0
- revenium_middleware/anthropic/summary_printer.py +286 -0
- revenium_middleware/anthropic/trace_fields.py +158 -0
- revenium_middleware/google/__init__.py +114 -0
- revenium_middleware/google/common/__init__.py +127 -0
- revenium_middleware/google/common/exceptions.py +137 -0
- revenium_middleware/google/common/protocols.py +192 -0
- revenium_middleware/google/common/summary_printer.py +271 -0
- revenium_middleware/google/common/trace_fields.py +205 -0
- revenium_middleware/google/common/types.py +208 -0
- revenium_middleware/google/common/utils.py +1111 -0
- revenium_middleware/google/config.py +64 -0
- revenium_middleware/google/google_ai/__init__.py +53 -0
- revenium_middleware/google/google_ai/middleware.py +667 -0
- revenium_middleware/google/google_ai/provider.py +135 -0
- revenium_middleware/google/prompt_extractor.py +396 -0
- revenium_middleware/google/vertex_ai/__init__.py +56 -0
- revenium_middleware/google/vertex_ai/middleware.py +1162 -0
- revenium_middleware/google/vertex_ai/provider.py +99 -0
- revenium_middleware/litellm/__init__.py +25 -0
- revenium_middleware/litellm/client/__init__.py +81 -0
- revenium_middleware/litellm/client/config.py +53 -0
- revenium_middleware/litellm/client/context.py +198 -0
- revenium_middleware/litellm/client/decorators.py +912 -0
- revenium_middleware/litellm/client/hooks.py +192 -0
- revenium_middleware/litellm/client/integrations/__init__.py +26 -0
- revenium_middleware/litellm/client/integrations/crewai.py +446 -0
- revenium_middleware/litellm/client/middleware.py +321 -0
- revenium_middleware/litellm/client/summary_printer.py +314 -0
- revenium_middleware/litellm/client/trace_fields.py +51 -0
- revenium_middleware/litellm/client/validation.py +207 -0
- revenium_middleware/litellm/proxy/__init__.py +25 -0
- revenium_middleware/litellm/proxy/middleware.py +217 -0
- revenium_middleware/ollama/__init__.py +28 -0
- revenium_middleware/ollama/middleware.py +569 -0
- revenium_middleware/ollama/trace_fields.py +63 -0
- revenium_middleware/openai/__init__.py +23 -0
- revenium_middleware/openai/azure_config.py +169 -0
- revenium_middleware/openai/azure_model_resolver.py +219 -0
- revenium_middleware/openai/config.py +45 -0
- revenium_middleware/openai/exceptions.py +115 -0
- revenium_middleware/openai/langchain/__init__.py +114 -0
- revenium_middleware/openai/langchain/_utils.py +129 -0
- revenium_middleware/openai/langchain/unified_handler.py +526 -0
- revenium_middleware/openai/middleware.py +1451 -0
- revenium_middleware/openai/prompt_extractor.py +173 -0
- revenium_middleware/openai/provider.py +170 -0
- revenium_middleware/openai/summary_printer.py +292 -0
- revenium_middleware/openai/trace_fields.py +98 -0
- revenium_middleware/perplexity/__init__.py +97 -0
- revenium_middleware/perplexity/middleware.py +379 -0
- revenium_middleware/perplexity/perplexity_sdk.py +256 -0
- revenium_middleware/perplexity/provider.py +84 -0
- revenium_middleware/perplexity/trace_fields.py +25 -0
- revenium_python_sdk-0.1.0.dist-info/METADATA +252 -0
- revenium_python_sdk-0.1.0.dist-info/RECORD +73 -0
- revenium_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- revenium_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- revenium_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import datetime
|
|
3
|
+
import wrapt
|
|
4
|
+
import types
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
7
|
+
|
|
8
|
+
from revenium_middleware import client, run_async_in_thread, shutdown_event, merge_metadata
|
|
9
|
+
from revenium_middleware._core.subscriber import extract_subscriber_from_metadata
|
|
10
|
+
from .trace_fields import (
|
|
11
|
+
get_environment,
|
|
12
|
+
get_region,
|
|
13
|
+
get_credential_alias,
|
|
14
|
+
get_trace_type,
|
|
15
|
+
get_trace_name,
|
|
16
|
+
get_parent_transaction_id,
|
|
17
|
+
get_transaction_name,
|
|
18
|
+
get_retry_number,
|
|
19
|
+
detect_operation_type
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def add_transaction_id_to_response(response, transaction_id):
|
|
24
|
+
"""
|
|
25
|
+
Add the Revenium transaction ID to an Ollama response object.
|
|
26
|
+
|
|
27
|
+
This function adds the transaction ID as an attribute to the response
|
|
28
|
+
object. The attribute can be accessed via response._revenium_transaction_id
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
response: The Ollama response object (ChatResponse or GenerateResponse)
|
|
32
|
+
transaction_id: The transaction ID string to add
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
# Add as attribute (works with response.attribute access)
|
|
36
|
+
# Ollama responses are Pydantic models, so we use setattr
|
|
37
|
+
setattr(response, '_revenium_transaction_id', transaction_id)
|
|
38
|
+
logger.debug(
|
|
39
|
+
"Added transaction ID %s to response",
|
|
40
|
+
transaction_id
|
|
41
|
+
)
|
|
42
|
+
except (TypeError, AttributeError) as e:
|
|
43
|
+
# If attribute setting doesn't work, log a warning
|
|
44
|
+
logger.warning(
|
|
45
|
+
"Could not add transaction ID as attribute: %s",
|
|
46
|
+
str(e)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_field_with_fallback(
|
|
51
|
+
usage_metadata, new_snake, new_camel, old_snake, old_camel, field_label
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Extract field with 4-level fallback and deprecation warning.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
usage_metadata: Dictionary containing usage metadata
|
|
58
|
+
new_snake: New snake_case field name (e.g., "organization_name")
|
|
59
|
+
new_camel: New camelCase field name (e.g., "organizationName")
|
|
60
|
+
old_snake: Old snake_case field name (e.g., "organization_id")
|
|
61
|
+
old_camel: Old camelCase field name (e.g., "organizationId")
|
|
62
|
+
field_label: Human-readable label for the field (e.g., "organization")
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The field value with precedence: new_snake > new_camel > old_snake > old_camel
|
|
66
|
+
"""
|
|
67
|
+
# Extract with precedence: new_snake > new_camel > old_snake > old_camel
|
|
68
|
+
value = usage_metadata.get(new_snake)
|
|
69
|
+
if value is None:
|
|
70
|
+
value = usage_metadata.get(new_camel)
|
|
71
|
+
if value is None:
|
|
72
|
+
value = usage_metadata.get(old_snake)
|
|
73
|
+
if value is None:
|
|
74
|
+
value = usage_metadata.get(old_camel)
|
|
75
|
+
|
|
76
|
+
# Log deprecation warning if old fields are used without new fields
|
|
77
|
+
if usage_metadata.get(old_snake) or usage_metadata.get(old_camel):
|
|
78
|
+
if not (usage_metadata.get(new_snake) or usage_metadata.get(new_camel)):
|
|
79
|
+
logger.warning(
|
|
80
|
+
f"Fields '{old_camel}' and '{old_snake}' are deprecated. "
|
|
81
|
+
f"Use '{new_camel}' or '{new_snake}' instead. "
|
|
82
|
+
"The old fields will be removed in a future version."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# extract_subscriber_from_metadata is imported from _core.subscriber
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@wrapt.patch_function_wrapper('ollama', 'chat')
|
|
93
|
+
def chat_wrapper(wrapped, _, args, kwargs):
|
|
94
|
+
"""
|
|
95
|
+
Wraps the ollama.chat method to log token usage.
|
|
96
|
+
Handles both streaming and non-streaming responses.
|
|
97
|
+
"""
|
|
98
|
+
logger.debug("Ollama chat wrapper called")
|
|
99
|
+
|
|
100
|
+
# Extract API-level metadata from kwargs
|
|
101
|
+
api_metadata = kwargs.pop("usage_metadata", {}) if "usage_metadata" in kwargs else {}
|
|
102
|
+
|
|
103
|
+
# Merge with decorator metadata (API-level takes precedence)
|
|
104
|
+
usage_metadata = merge_metadata(api_metadata)
|
|
105
|
+
|
|
106
|
+
is_streaming = kwargs.get("stream", False)
|
|
107
|
+
|
|
108
|
+
# If streaming is enabled, add stream_options to include usage information
|
|
109
|
+
# if is_streaming and "stream_options" not in kwargs:
|
|
110
|
+
# kwargs["stream_options"] = {"include_usage": True}
|
|
111
|
+
# elif is_streaming and isinstance(kwargs.get("stream_options"), dict):
|
|
112
|
+
# kwargs["stream_options"]["include_usage"] = True
|
|
113
|
+
|
|
114
|
+
request_time_dt = datetime.datetime.now(datetime.timezone.utc)
|
|
115
|
+
# Generate transaction ID using the same timestamp for consistency
|
|
116
|
+
transaction_id = f"ollama-{request_time_dt.timestamp()}"
|
|
117
|
+
|
|
118
|
+
logger.debug(f"Calling chat function with args: {args}, kwargs: {kwargs}")
|
|
119
|
+
|
|
120
|
+
response = wrapped(*args, **kwargs)
|
|
121
|
+
|
|
122
|
+
# Check if response is a generator (streaming response)
|
|
123
|
+
if is_streaming and isinstance(response, types.GeneratorType):
|
|
124
|
+
return handle_streaming_response(
|
|
125
|
+
response, request_time_dt, usage_metadata,
|
|
126
|
+
transaction_id, 'chat', kwargs
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
# Handle non-streaming response
|
|
130
|
+
logger.debug("Ollama chat response: %s", response)
|
|
131
|
+
|
|
132
|
+
# Add transaction ID to response object
|
|
133
|
+
add_transaction_id_to_response(response, transaction_id)
|
|
134
|
+
|
|
135
|
+
handle_response(
|
|
136
|
+
response, request_time_dt, usage_metadata,
|
|
137
|
+
False, transaction_id, 'chat', kwargs
|
|
138
|
+
)
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@wrapt.patch_function_wrapper('ollama', 'generate')
|
|
143
|
+
def generate_wrapper(wrapped, _, args, kwargs):
|
|
144
|
+
"""
|
|
145
|
+
Wraps the ollama.generate method to log token usage.
|
|
146
|
+
Handles both streaming and non-streaming responses.
|
|
147
|
+
"""
|
|
148
|
+
logger.debug("Ollama generate wrapper called")
|
|
149
|
+
|
|
150
|
+
# Extract API-level metadata from kwargs
|
|
151
|
+
api_metadata = kwargs.pop("usage_metadata", {}) if "usage_metadata" in kwargs else {}
|
|
152
|
+
|
|
153
|
+
# Merge with decorator metadata (API-level takes precedence)
|
|
154
|
+
usage_metadata = merge_metadata(api_metadata)
|
|
155
|
+
|
|
156
|
+
is_streaming = kwargs.get("stream", False)
|
|
157
|
+
|
|
158
|
+
# Note: ollama.generate() doesn't support stream_options parameter
|
|
159
|
+
# Token usage is included by default in the final chunk
|
|
160
|
+
|
|
161
|
+
request_time_dt = datetime.datetime.now(datetime.timezone.utc)
|
|
162
|
+
# Generate transaction ID using the same timestamp for consistency
|
|
163
|
+
transaction_id = f"ollama-{request_time_dt.timestamp()}"
|
|
164
|
+
|
|
165
|
+
logger.debug(f"Calling generate function with args: {args}, kwargs: {kwargs}")
|
|
166
|
+
|
|
167
|
+
response = wrapped(*args, **kwargs)
|
|
168
|
+
|
|
169
|
+
# Check if response is a generator (streaming response)
|
|
170
|
+
if is_streaming and isinstance(response, types.GeneratorType):
|
|
171
|
+
return handle_streaming_response(
|
|
172
|
+
response, request_time_dt, usage_metadata,
|
|
173
|
+
transaction_id, 'generate', kwargs
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
# Handle non-streaming response
|
|
177
|
+
logger.debug("Ollama generate response: %s", response)
|
|
178
|
+
|
|
179
|
+
# Add transaction ID to response object
|
|
180
|
+
add_transaction_id_to_response(response, transaction_id)
|
|
181
|
+
|
|
182
|
+
handle_response(
|
|
183
|
+
response, request_time_dt, usage_metadata,
|
|
184
|
+
False, transaction_id, 'generate', kwargs
|
|
185
|
+
)
|
|
186
|
+
return response
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@wrapt.patch_function_wrapper('ollama', 'embed')
|
|
190
|
+
def embed_wrapper(wrapped, _, args, kwargs):
|
|
191
|
+
"""
|
|
192
|
+
Wraps the ollama.embed method to log token usage for embeddings.
|
|
193
|
+
Handles both single and batch embedding requests.
|
|
194
|
+
"""
|
|
195
|
+
logger.debug("Ollama embed wrapper called")
|
|
196
|
+
usage_metadata = kwargs.pop("usage_metadata", {}) if "usage_metadata" in kwargs else {}
|
|
197
|
+
|
|
198
|
+
request_time_dt = datetime.datetime.now(datetime.timezone.utc)
|
|
199
|
+
# Generate transaction ID using the same timestamp for consistency
|
|
200
|
+
transaction_id = f"ollama-{request_time_dt.timestamp()}"
|
|
201
|
+
|
|
202
|
+
logger.debug(f"Calling embed function with args: {args}, kwargs: {kwargs}")
|
|
203
|
+
|
|
204
|
+
response = wrapped(*args, **kwargs)
|
|
205
|
+
|
|
206
|
+
# Handle embeddings response (embeddings are not streamed)
|
|
207
|
+
logger.debug("Ollama embed response: %s", response)
|
|
208
|
+
|
|
209
|
+
# Add transaction ID to response object
|
|
210
|
+
add_transaction_id_to_response(response, transaction_id)
|
|
211
|
+
|
|
212
|
+
# Handle embeddings response - embeddings only have input tokens, no output tokens
|
|
213
|
+
handle_embeddings_response(
|
|
214
|
+
response, request_time_dt, usage_metadata,
|
|
215
|
+
transaction_id, kwargs
|
|
216
|
+
)
|
|
217
|
+
return response
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def handle_streaming_response(
|
|
221
|
+
generator,
|
|
222
|
+
request_time_dt,
|
|
223
|
+
usage_metadata,
|
|
224
|
+
transaction_id,
|
|
225
|
+
endpoint,
|
|
226
|
+
request_kwargs
|
|
227
|
+
):
|
|
228
|
+
"""
|
|
229
|
+
Handles streaming responses by collecting all chunks and processing the
|
|
230
|
+
final state. Returns a new generator that yields the same chunks with
|
|
231
|
+
transaction IDs added.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
generator: The original response generator
|
|
235
|
+
request_time_dt: The request timestamp
|
|
236
|
+
usage_metadata: Metadata for metering
|
|
237
|
+
transaction_id: The transaction ID to add to responses
|
|
238
|
+
endpoint: The endpoint being called ('chat', 'generate', etc.)
|
|
239
|
+
request_kwargs: The request kwargs for operation type detection
|
|
240
|
+
"""
|
|
241
|
+
chunks = []
|
|
242
|
+
final_response = None
|
|
243
|
+
|
|
244
|
+
def wrapped_generator():
|
|
245
|
+
nonlocal final_response
|
|
246
|
+
|
|
247
|
+
# Collect all chunks and add transaction ID to each
|
|
248
|
+
for chunk in generator:
|
|
249
|
+
chunks.append(chunk)
|
|
250
|
+
# Add transaction ID to each chunk
|
|
251
|
+
add_transaction_id_to_response(chunk, transaction_id)
|
|
252
|
+
yield chunk
|
|
253
|
+
|
|
254
|
+
# After all chunks are processed, construct the final response
|
|
255
|
+
if chunks:
|
|
256
|
+
# The last chunk should contain the complete response data
|
|
257
|
+
final_response = chunks[-1]
|
|
258
|
+
handle_response(
|
|
259
|
+
final_response,
|
|
260
|
+
request_time_dt,
|
|
261
|
+
usage_metadata,
|
|
262
|
+
True,
|
|
263
|
+
transaction_id,
|
|
264
|
+
endpoint,
|
|
265
|
+
request_kwargs
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return wrapped_generator()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def handle_response(
|
|
272
|
+
response,
|
|
273
|
+
request_time_dt,
|
|
274
|
+
usage_metadata,
|
|
275
|
+
is_streaming,
|
|
276
|
+
transaction_id,
|
|
277
|
+
endpoint,
|
|
278
|
+
request_kwargs
|
|
279
|
+
):
|
|
280
|
+
"""
|
|
281
|
+
Process a complete response (either streaming or non-streaming) and
|
|
282
|
+
send metering data.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
response: The Ollama response object
|
|
286
|
+
request_time_dt: The request timestamp
|
|
287
|
+
usage_metadata: Metadata for metering
|
|
288
|
+
is_streaming: Whether this is a streaming response
|
|
289
|
+
transaction_id: The transaction ID for this request
|
|
290
|
+
endpoint: The endpoint being called ('chat', 'generate', etc.)
|
|
291
|
+
request_kwargs: The request kwargs for operation type detection
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
async def metering_call():
|
|
295
|
+
response_time_dt = datetime.datetime.now(datetime.timezone.utc)
|
|
296
|
+
response_time = response_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
297
|
+
request_duration = (
|
|
298
|
+
(response_time_dt - request_time_dt).total_seconds() * 1000
|
|
299
|
+
)
|
|
300
|
+
request_time = request_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
301
|
+
|
|
302
|
+
# Use the provided transaction ID
|
|
303
|
+
response_id = transaction_id
|
|
304
|
+
|
|
305
|
+
# Extract token counts from Ollama response
|
|
306
|
+
prompt_tokens = getattr(response, 'prompt_eval_count', 0)
|
|
307
|
+
completion_tokens = getattr(response, 'eval_count', 0)
|
|
308
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
309
|
+
cached_tokens = 0 # Ollama doesn't provide cached tokens info
|
|
310
|
+
|
|
311
|
+
logger.debug(
|
|
312
|
+
"Ollama chat token usage - prompt: %d, completion: %d, total: %d",
|
|
313
|
+
prompt_tokens, completion_tokens, total_tokens
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
ollama_finish_reason = getattr(response, 'done_reason', None)
|
|
317
|
+
|
|
318
|
+
finish_reason_map = {
|
|
319
|
+
"stop": "END",
|
|
320
|
+
"length": "TOKEN_LIMIT",
|
|
321
|
+
"error": "ERROR",
|
|
322
|
+
"cancelled": "CANCELLED", # British spelling
|
|
323
|
+
"canceled": "CANCELLED", # American spelling (Go standard library uses this)
|
|
324
|
+
"tool_calls": "END_SEQUENCE"
|
|
325
|
+
}
|
|
326
|
+
stop_reason = finish_reason_map.get(ollama_finish_reason, "END") # type: ignore
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
if shutdown_event.is_set():
|
|
330
|
+
logger.warning("Skipping metering call during shutdown")
|
|
331
|
+
return
|
|
332
|
+
logger.debug("Metering call to Revenium for completion %s", response_id)
|
|
333
|
+
|
|
334
|
+
# Create subscriber object from usage metadata
|
|
335
|
+
subscriber = extract_subscriber_from_metadata(usage_metadata)
|
|
336
|
+
|
|
337
|
+
# Detect operation type
|
|
338
|
+
operation_type = detect_operation_type(endpoint, request_kwargs)
|
|
339
|
+
|
|
340
|
+
# Capture trace visualization fields
|
|
341
|
+
environment = get_environment()
|
|
342
|
+
region = get_region()
|
|
343
|
+
credential_alias = get_credential_alias()
|
|
344
|
+
trace_type = get_trace_type()
|
|
345
|
+
trace_name = get_trace_name()
|
|
346
|
+
parent_transaction_id = get_parent_transaction_id()
|
|
347
|
+
transaction_name = get_transaction_name(usage_metadata)
|
|
348
|
+
retry_number = get_retry_number()
|
|
349
|
+
|
|
350
|
+
# Extract organization and product names with deprecation warnings
|
|
351
|
+
organization_name = extract_field_with_fallback(
|
|
352
|
+
usage_metadata,
|
|
353
|
+
"organization_name",
|
|
354
|
+
"organizationName",
|
|
355
|
+
"organization_id",
|
|
356
|
+
"organizationId",
|
|
357
|
+
"organization",
|
|
358
|
+
)
|
|
359
|
+
product_name = extract_field_with_fallback(
|
|
360
|
+
usage_metadata,
|
|
361
|
+
"product_name",
|
|
362
|
+
"productName",
|
|
363
|
+
"product_id",
|
|
364
|
+
"productId",
|
|
365
|
+
"product",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Prepare arguments for create_completion
|
|
369
|
+
completion_args = {
|
|
370
|
+
"cache_creation_token_count": cached_tokens,
|
|
371
|
+
"cache_read_token_count": 0,
|
|
372
|
+
"input_token_cost": None,
|
|
373
|
+
"output_token_cost": None,
|
|
374
|
+
"total_cost": None,
|
|
375
|
+
"output_token_count": completion_tokens,
|
|
376
|
+
"cost_type": "AI",
|
|
377
|
+
"model": getattr(response, 'model', 'ollama-model'),
|
|
378
|
+
"input_token_count": prompt_tokens,
|
|
379
|
+
"provider": "OLLAMA",
|
|
380
|
+
"model_source": "OLLAMA",
|
|
381
|
+
"reasoning_token_count": 0,
|
|
382
|
+
"request_time": request_time,
|
|
383
|
+
"response_time": response_time,
|
|
384
|
+
"completion_start_time": response_time,
|
|
385
|
+
"request_duration": int(request_duration),
|
|
386
|
+
"stop_reason": stop_reason,
|
|
387
|
+
"total_token_count": total_tokens,
|
|
388
|
+
"transaction_id": response_id,
|
|
389
|
+
"trace_id": usage_metadata.get("trace_id"),
|
|
390
|
+
"task_type": usage_metadata.get("task_type"),
|
|
391
|
+
"subscriber": subscriber if subscriber else None,
|
|
392
|
+
"organization_name": organization_name,
|
|
393
|
+
"subscription_id": usage_metadata.get("subscription_id"),
|
|
394
|
+
"product_name": product_name,
|
|
395
|
+
"agent": usage_metadata.get("agent"),
|
|
396
|
+
"response_quality_score": usage_metadata.get("response_quality_score"),
|
|
397
|
+
"is_streamed": is_streaming,
|
|
398
|
+
"middleware_source": "PYTHON",
|
|
399
|
+
# Trace visualization fields
|
|
400
|
+
"operation_type": operation_type,
|
|
401
|
+
"environment": environment,
|
|
402
|
+
"region": region,
|
|
403
|
+
"credential_alias": credential_alias,
|
|
404
|
+
"trace_type": trace_type,
|
|
405
|
+
"trace_name": trace_name,
|
|
406
|
+
"parent_transaction_id": parent_transaction_id,
|
|
407
|
+
"transaction_name": transaction_name,
|
|
408
|
+
"retry_number": retry_number
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Log the arguments at debug level
|
|
412
|
+
logger.debug("Arguments for create_completion: %s", completion_args)
|
|
413
|
+
|
|
414
|
+
# The client.ai.create_completion method is not async, so don't use await
|
|
415
|
+
result = client.ai.create_completion(**completion_args)
|
|
416
|
+
logger.debug("Metering call result: %s", result)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
if not shutdown_event.is_set():
|
|
419
|
+
logger.warning(f"Error in metering call: {str(e)}")
|
|
420
|
+
# Log the full traceback for better debugging
|
|
421
|
+
import traceback
|
|
422
|
+
logger.warning(f"Traceback: {traceback.format_exc()}")
|
|
423
|
+
|
|
424
|
+
thread = run_async_in_thread(metering_call())
|
|
425
|
+
logger.debug("Metering thread started: %s", thread)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def handle_embeddings_response(
|
|
429
|
+
response,
|
|
430
|
+
request_time_dt,
|
|
431
|
+
usage_metadata,
|
|
432
|
+
transaction_id,
|
|
433
|
+
request_kwargs
|
|
434
|
+
):
|
|
435
|
+
"""
|
|
436
|
+
Process an embeddings response and send metering data.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
response: The Ollama embeddings response object
|
|
440
|
+
request_time_dt: The request timestamp
|
|
441
|
+
usage_metadata: Metadata for metering
|
|
442
|
+
transaction_id: The transaction ID for this request
|
|
443
|
+
request_kwargs: The request kwargs for operation type detection
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
async def metering_call():
|
|
447
|
+
response_time_dt = datetime.datetime.now(datetime.timezone.utc)
|
|
448
|
+
response_time = response_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
449
|
+
request_duration = (
|
|
450
|
+
(response_time_dt - request_time_dt).total_seconds() * 1000
|
|
451
|
+
)
|
|
452
|
+
request_time = request_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
453
|
+
|
|
454
|
+
# Use the provided transaction ID
|
|
455
|
+
response_id = transaction_id
|
|
456
|
+
|
|
457
|
+
# Extract token counts from Ollama embeddings response
|
|
458
|
+
# Embeddings only have input tokens (prompt_eval_count), no output tokens
|
|
459
|
+
prompt_tokens = getattr(response, 'prompt_eval_count', 0)
|
|
460
|
+
completion_tokens = 0 # Embeddings don't generate output tokens
|
|
461
|
+
total_tokens = prompt_tokens
|
|
462
|
+
cached_tokens = 0 # Ollama doesn't provide cached tokens info
|
|
463
|
+
|
|
464
|
+
logger.debug(
|
|
465
|
+
"Ollama embeddings token usage - prompt: %d, total: %d",
|
|
466
|
+
prompt_tokens, total_tokens
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Embeddings always complete successfully (no finish reason)
|
|
470
|
+
stop_reason = "END"
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
if shutdown_event.is_set():
|
|
474
|
+
logger.warning("Skipping metering call during shutdown")
|
|
475
|
+
return
|
|
476
|
+
logger.debug("Metering call to Revenium for embeddings %s", response_id)
|
|
477
|
+
|
|
478
|
+
# Create subscriber object from usage metadata
|
|
479
|
+
subscriber = extract_subscriber_from_metadata(usage_metadata)
|
|
480
|
+
|
|
481
|
+
# Detect operation type - should be 'EMBED' for embeddings
|
|
482
|
+
operation_type = detect_operation_type('embed', request_kwargs)
|
|
483
|
+
|
|
484
|
+
# Capture trace visualization fields
|
|
485
|
+
environment = get_environment()
|
|
486
|
+
region = get_region()
|
|
487
|
+
credential_alias = get_credential_alias()
|
|
488
|
+
trace_type = get_trace_type()
|
|
489
|
+
trace_name = get_trace_name()
|
|
490
|
+
parent_transaction_id = get_parent_transaction_id()
|
|
491
|
+
transaction_name = get_transaction_name(usage_metadata)
|
|
492
|
+
retry_number = get_retry_number()
|
|
493
|
+
|
|
494
|
+
# Extract organization and product names with deprecation warnings
|
|
495
|
+
organization_name = extract_field_with_fallback(
|
|
496
|
+
usage_metadata,
|
|
497
|
+
"organization_name",
|
|
498
|
+
"organizationName",
|
|
499
|
+
"organization_id",
|
|
500
|
+
"organizationId",
|
|
501
|
+
"organization",
|
|
502
|
+
)
|
|
503
|
+
product_name = extract_field_with_fallback(
|
|
504
|
+
usage_metadata,
|
|
505
|
+
"product_name",
|
|
506
|
+
"productName",
|
|
507
|
+
"product_id",
|
|
508
|
+
"productId",
|
|
509
|
+
"product",
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Prepare arguments for create_completion
|
|
513
|
+
completion_args = {
|
|
514
|
+
"cache_creation_token_count": cached_tokens,
|
|
515
|
+
"cache_read_token_count": 0,
|
|
516
|
+
"input_token_cost": None,
|
|
517
|
+
"output_token_cost": None,
|
|
518
|
+
"total_cost": None,
|
|
519
|
+
"output_token_count": completion_tokens,
|
|
520
|
+
"cost_type": "AI",
|
|
521
|
+
"model": getattr(response, 'model', 'ollama-model'),
|
|
522
|
+
"input_token_count": prompt_tokens,
|
|
523
|
+
"provider": "OLLAMA",
|
|
524
|
+
"model_source": "OLLAMA",
|
|
525
|
+
"reasoning_token_count": 0,
|
|
526
|
+
"request_time": request_time,
|
|
527
|
+
"response_time": response_time,
|
|
528
|
+
"completion_start_time": response_time,
|
|
529
|
+
"request_duration": int(request_duration),
|
|
530
|
+
"stop_reason": stop_reason,
|
|
531
|
+
"total_token_count": total_tokens,
|
|
532
|
+
"transaction_id": response_id,
|
|
533
|
+
"trace_id": usage_metadata.get("trace_id"),
|
|
534
|
+
"task_type": usage_metadata.get("task_type"),
|
|
535
|
+
"subscriber": subscriber if subscriber else None,
|
|
536
|
+
"organization_name": organization_name,
|
|
537
|
+
"subscription_id": usage_metadata.get("subscription_id"),
|
|
538
|
+
"product_name": product_name,
|
|
539
|
+
"agent": usage_metadata.get("agent"),
|
|
540
|
+
"response_quality_score": usage_metadata.get("response_quality_score"),
|
|
541
|
+
"is_streamed": False, # Embeddings are never streamed
|
|
542
|
+
"middleware_source": "PYTHON",
|
|
543
|
+
# Trace visualization fields
|
|
544
|
+
"operation_type": operation_type,
|
|
545
|
+
"environment": environment,
|
|
546
|
+
"region": region,
|
|
547
|
+
"credential_alias": credential_alias,
|
|
548
|
+
"trace_type": trace_type,
|
|
549
|
+
"trace_name": trace_name,
|
|
550
|
+
"parent_transaction_id": parent_transaction_id,
|
|
551
|
+
"transaction_name": transaction_name,
|
|
552
|
+
"retry_number": retry_number
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Log the arguments at debug level
|
|
556
|
+
logger.debug("Arguments for create_completion: %s", completion_args)
|
|
557
|
+
|
|
558
|
+
# The client.ai.create_completion method is not async, so don't use await
|
|
559
|
+
result = client.ai.create_completion(**completion_args)
|
|
560
|
+
logger.debug("Metering call result: %s", result)
|
|
561
|
+
except Exception as e:
|
|
562
|
+
if not shutdown_event.is_set():
|
|
563
|
+
logger.warning(f"Error in metering call: {str(e)}")
|
|
564
|
+
# Log the full traceback for better debugging
|
|
565
|
+
import traceback
|
|
566
|
+
logger.warning(f"Traceback: {traceback.format_exc()}")
|
|
567
|
+
|
|
568
|
+
thread = run_async_in_thread(metering_call())
|
|
569
|
+
logger.debug("Metering thread started: %s", thread)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace visualization field capture and validation.
|
|
3
|
+
|
|
4
|
+
This module provides functions to capture trace visualization fields from
|
|
5
|
+
environment variables and validate them according to the specification.
|
|
6
|
+
|
|
7
|
+
Shared functions are imported from _core.trace_fields. This module retains
|
|
8
|
+
only the Ollama-specific detect_operation_type function.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
|
|
13
|
+
from revenium_middleware._core.trace_fields import ( # noqa: F401 — re-exported
|
|
14
|
+
TRACE_TYPE_MAX_LENGTH,
|
|
15
|
+
TRACE_NAME_MAX_LENGTH,
|
|
16
|
+
TRACE_TYPE_PATTERN,
|
|
17
|
+
get_environment,
|
|
18
|
+
get_region,
|
|
19
|
+
get_credential_alias,
|
|
20
|
+
get_trace_type,
|
|
21
|
+
get_trace_name,
|
|
22
|
+
get_parent_transaction_id,
|
|
23
|
+
get_transaction_name,
|
|
24
|
+
get_retry_number,
|
|
25
|
+
validate_trace_type,
|
|
26
|
+
validate_trace_name,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def detect_operation_type(
|
|
31
|
+
endpoint: str,
|
|
32
|
+
request_body: Optional[Dict[str, Any]] = None
|
|
33
|
+
) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Auto-detect operation type from endpoint and request.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
endpoint: API endpoint (e.g., 'chat', 'generate', 'embeddings')
|
|
39
|
+
request_body: Optional request body to check for tools
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Operation type string ('CHAT', 'GENERATE', 'EMBED', 'TOOL_CALL')
|
|
43
|
+
"""
|
|
44
|
+
request_body = request_body or {}
|
|
45
|
+
|
|
46
|
+
# Chat endpoint
|
|
47
|
+
if endpoint == 'chat':
|
|
48
|
+
# Check for tools in request
|
|
49
|
+
has_tools = request_body.get('tools')
|
|
50
|
+
if has_tools:
|
|
51
|
+
return 'TOOL_CALL'
|
|
52
|
+
return 'CHAT'
|
|
53
|
+
|
|
54
|
+
# Generate endpoint
|
|
55
|
+
if endpoint == 'generate':
|
|
56
|
+
return 'GENERATE'
|
|
57
|
+
|
|
58
|
+
# Embeddings endpoint
|
|
59
|
+
if endpoint in ('embeddings', 'embed'):
|
|
60
|
+
return 'EMBED'
|
|
61
|
+
|
|
62
|
+
# Default fallback
|
|
63
|
+
return 'CHAT'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Revenium middleware for OpenAI and Azure OpenAI.
|
|
3
|
+
|
|
4
|
+
Automatically hooks openai.ChatCompletion.create using wrapt and logs
|
|
5
|
+
token usage after each request. Supports OpenAI, Azure OpenAI, and
|
|
6
|
+
LangChain integrations.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
import revenium_middleware.openai # auto-instruments OpenAI SDK
|
|
10
|
+
"""
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Conditionally import middleware (requires wrapt + openai SDK)
|
|
16
|
+
try:
|
|
17
|
+
import wrapt # noqa: F401
|
|
18
|
+
from .middleware import create_wrapper
|
|
19
|
+
except ImportError:
|
|
20
|
+
logger.debug("OpenAI middleware dependencies not available, middleware not loaded")
|
|
21
|
+
create_wrapper = None # type: ignore
|
|
22
|
+
|
|
23
|
+
__all__ = ["create_wrapper"]
|