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.
Files changed (73) hide show
  1. revenium_middleware/__init__.py +184 -0
  2. revenium_middleware/_core/__init__.py +65 -0
  3. revenium_middleware/_core/config.py +165 -0
  4. revenium_middleware/_core/context.py +109 -0
  5. revenium_middleware/_core/decorators.py +202 -0
  6. revenium_middleware/_core/metering.py +207 -0
  7. revenium_middleware/_core/prompt_extraction.py +55 -0
  8. revenium_middleware/_core/subscriber.py +51 -0
  9. revenium_middleware/_core/trace_fields.py +265 -0
  10. revenium_middleware/anthropic/__init__.py +108 -0
  11. revenium_middleware/anthropic/bedrock_adapter.py +753 -0
  12. revenium_middleware/anthropic/config.py +29 -0
  13. revenium_middleware/anthropic/middleware.py +1070 -0
  14. revenium_middleware/anthropic/prompt_extractor.py +178 -0
  15. revenium_middleware/anthropic/provider.py +141 -0
  16. revenium_middleware/anthropic/summary_printer.py +286 -0
  17. revenium_middleware/anthropic/trace_fields.py +158 -0
  18. revenium_middleware/google/__init__.py +114 -0
  19. revenium_middleware/google/common/__init__.py +127 -0
  20. revenium_middleware/google/common/exceptions.py +137 -0
  21. revenium_middleware/google/common/protocols.py +192 -0
  22. revenium_middleware/google/common/summary_printer.py +271 -0
  23. revenium_middleware/google/common/trace_fields.py +205 -0
  24. revenium_middleware/google/common/types.py +208 -0
  25. revenium_middleware/google/common/utils.py +1111 -0
  26. revenium_middleware/google/config.py +64 -0
  27. revenium_middleware/google/google_ai/__init__.py +53 -0
  28. revenium_middleware/google/google_ai/middleware.py +667 -0
  29. revenium_middleware/google/google_ai/provider.py +135 -0
  30. revenium_middleware/google/prompt_extractor.py +396 -0
  31. revenium_middleware/google/vertex_ai/__init__.py +56 -0
  32. revenium_middleware/google/vertex_ai/middleware.py +1162 -0
  33. revenium_middleware/google/vertex_ai/provider.py +99 -0
  34. revenium_middleware/litellm/__init__.py +25 -0
  35. revenium_middleware/litellm/client/__init__.py +81 -0
  36. revenium_middleware/litellm/client/config.py +53 -0
  37. revenium_middleware/litellm/client/context.py +198 -0
  38. revenium_middleware/litellm/client/decorators.py +912 -0
  39. revenium_middleware/litellm/client/hooks.py +192 -0
  40. revenium_middleware/litellm/client/integrations/__init__.py +26 -0
  41. revenium_middleware/litellm/client/integrations/crewai.py +446 -0
  42. revenium_middleware/litellm/client/middleware.py +321 -0
  43. revenium_middleware/litellm/client/summary_printer.py +314 -0
  44. revenium_middleware/litellm/client/trace_fields.py +51 -0
  45. revenium_middleware/litellm/client/validation.py +207 -0
  46. revenium_middleware/litellm/proxy/__init__.py +25 -0
  47. revenium_middleware/litellm/proxy/middleware.py +217 -0
  48. revenium_middleware/ollama/__init__.py +28 -0
  49. revenium_middleware/ollama/middleware.py +569 -0
  50. revenium_middleware/ollama/trace_fields.py +63 -0
  51. revenium_middleware/openai/__init__.py +23 -0
  52. revenium_middleware/openai/azure_config.py +169 -0
  53. revenium_middleware/openai/azure_model_resolver.py +219 -0
  54. revenium_middleware/openai/config.py +45 -0
  55. revenium_middleware/openai/exceptions.py +115 -0
  56. revenium_middleware/openai/langchain/__init__.py +114 -0
  57. revenium_middleware/openai/langchain/_utils.py +129 -0
  58. revenium_middleware/openai/langchain/unified_handler.py +526 -0
  59. revenium_middleware/openai/middleware.py +1451 -0
  60. revenium_middleware/openai/prompt_extractor.py +173 -0
  61. revenium_middleware/openai/provider.py +170 -0
  62. revenium_middleware/openai/summary_printer.py +292 -0
  63. revenium_middleware/openai/trace_fields.py +98 -0
  64. revenium_middleware/perplexity/__init__.py +97 -0
  65. revenium_middleware/perplexity/middleware.py +379 -0
  66. revenium_middleware/perplexity/perplexity_sdk.py +256 -0
  67. revenium_middleware/perplexity/provider.py +84 -0
  68. revenium_middleware/perplexity/trace_fields.py +25 -0
  69. revenium_python_sdk-0.1.0.dist-info/METADATA +252 -0
  70. revenium_python_sdk-0.1.0.dist-info/RECORD +73 -0
  71. revenium_python_sdk-0.1.0.dist-info/WHEEL +5 -0
  72. revenium_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  73. 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"]