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,98 @@
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 OpenAI-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
+ provider: str,
32
+ endpoint: str,
33
+ request_body: Optional[Dict[str, Any]] = None
34
+ ) -> Dict[str, Optional[str]]:
35
+ """
36
+ Auto-detect operation type and subtype from provider, endpoint,
37
+ and request.
38
+
39
+ Args:
40
+ provider: Provider name (e.g., 'openai', 'azure_openai') or Provider enum
41
+ endpoint: API endpoint (e.g., '/chat/completions', '/embeddings')
42
+ request_body: Optional request body to check for tools/functions
43
+
44
+ Returns:
45
+ Dictionary with 'operationType' and 'operationSubtype' keys
46
+ """
47
+ # Handle Provider enum or string
48
+ if hasattr(provider, 'name'):
49
+ # It's a Provider enum, get the name (e.g., 'OPENAI', 'AZURE_OPENAI')
50
+ provider_str = provider.name
51
+ else:
52
+ provider_str = str(provider)
53
+
54
+ provider_lower = provider_str.lower()
55
+ request_body = request_body or {}
56
+
57
+ # OpenAI and Azure OpenAI
58
+ if provider_lower in ('openai', 'azure_openai', 'azure'):
59
+ # Chat completions
60
+ is_chat = (
61
+ 'chat/completions' in endpoint or
62
+ endpoint.endswith('/chat/completions')
63
+ )
64
+ if is_chat:
65
+ # Check for tools or functions
66
+ has_tools = (
67
+ request_body.get('tools') or
68
+ request_body.get('functions')
69
+ )
70
+ if has_tools:
71
+ return {
72
+ 'operationType': 'TOOL_CALL',
73
+ 'operationSubtype': 'function_call'
74
+ }
75
+ return {
76
+ 'operationType': 'CHAT',
77
+ 'operationSubtype': None
78
+ }
79
+
80
+ # Embeddings
81
+ if 'embeddings' in endpoint or endpoint.endswith('/embeddings'):
82
+ return {
83
+ 'operationType': 'EMBED',
84
+ 'operationSubtype': None
85
+ }
86
+
87
+ # Moderations
88
+ if 'moderations' in endpoint or endpoint.endswith('/moderations'):
89
+ return {
90
+ 'operationType': 'MODERATION',
91
+ 'operationSubtype': None
92
+ }
93
+
94
+ # Default fallback
95
+ return {
96
+ 'operationType': 'CHAT',
97
+ 'operationSubtype': None
98
+ }
@@ -0,0 +1,97 @@
1
+ """
2
+ Revenium Middleware for Perplexity AI
3
+
4
+ This library automatically tracks Perplexity AI API usage and sends
5
+ metering data to Revenium. Simply import this module and all Perplexity
6
+ API calls will be automatically tracked.
7
+
8
+ Supports both OpenAI SDK and native Perplexity SDK:
9
+
10
+ Example 1 - OpenAI SDK:
11
+ import revenium_middleware.perplexity
12
+ from openai import OpenAI
13
+
14
+ client = OpenAI(
15
+ api_key="your-perplexity-api-key",
16
+ base_url="https://api.perplexity.ai"
17
+ )
18
+
19
+ response = client.chat.completions.create(
20
+ model="sonar",
21
+ messages=[{"role": "user", "content": "Hello!"}]
22
+ )
23
+
24
+ Example 2 - Native Perplexity SDK:
25
+ import revenium_middleware.perplexity
26
+ from perplexity import Perplexity
27
+
28
+ client = Perplexity(api_key="your-perplexity-api-key")
29
+
30
+ response = client.chat.completions.create(
31
+ model="sonar",
32
+ messages=[{"role": "user", "content": "Hello!"}]
33
+ )
34
+
35
+ Example 3 - With Decorators:
36
+ from revenium_middleware.perplexity import revenium_metadata
37
+ from openai import OpenAI
38
+
39
+ client = OpenAI(
40
+ api_key="your-perplexity-api-key",
41
+ base_url="https://api.perplexity.ai"
42
+ )
43
+
44
+ @revenium_metadata(
45
+ organization_id="acme-corp",
46
+ task_type="analysis"
47
+ )
48
+ def analyze_text(text):
49
+ # Metadata automatically injected!
50
+ response = client.chat.completions.create(
51
+ model="sonar",
52
+ messages=[{"role": "user", "content": text}]
53
+ )
54
+ return response.choices[0].message.content
55
+ """
56
+ import logging
57
+
58
+ logger = logging.getLogger("revenium_middleware.perplexity")
59
+
60
+ # Conditionally import middleware (requires wrapt)
61
+ try:
62
+ import wrapt # noqa: F401
63
+ from .middleware import create_wrapper
64
+ from .perplexity_sdk import perplexity_create_wrapper
65
+ except ImportError:
66
+ logger.debug("Perplexity middleware dependencies not available, middleware not loaded")
67
+ create_wrapper = None # type: ignore
68
+ perplexity_create_wrapper = None # type: ignore
69
+
70
+ # Re-export decorators from revenium_middleware for convenience
71
+ try:
72
+ from revenium_middleware import revenium_metadata, revenium_meter
73
+ _decorators_available = True
74
+ except ImportError:
75
+ _decorators_available = False
76
+ revenium_metadata = None # type: ignore
77
+ revenium_meter = None # type: ignore
78
+
79
+ # Both wrappers are automatically applied when this module is imported
80
+ # via the @wrapt.patch_function_wrapper decorators
81
+
82
+ __version__ = "0.1.0"
83
+
84
+ if _decorators_available:
85
+ __all__ = [
86
+ "create_wrapper",
87
+ "perplexity_create_wrapper",
88
+ "revenium_metadata",
89
+ "revenium_meter"
90
+ ]
91
+ else:
92
+ __all__ = ["create_wrapper", "perplexity_create_wrapper"]
93
+
94
+ logger.debug(
95
+ "Revenium Perplexity middleware loaded - "
96
+ "both OpenAI and Perplexity SDK wrappers active"
97
+ )
@@ -0,0 +1,379 @@
1
+ """
2
+ Revenium Middleware for Perplexity AI
3
+
4
+ This module provides automatic metering and tracking for Perplexity AI API
5
+ calls. It uses wrapt to patch the OpenAI client methods and send usage data
6
+ to Revenium.
7
+ """
8
+ import datetime
9
+ import logging
10
+ import uuid
11
+ from typing import Dict, Any, Optional, Iterator
12
+ from enum import Enum
13
+
14
+ import wrapt
15
+ from revenium_middleware import (
16
+ client,
17
+ run_async_in_thread,
18
+ shutdown_event,
19
+ merge_metadata,
20
+ )
21
+
22
+ from .provider import Provider, detect_provider, get_provider_metadata
23
+ from .trace_fields import (
24
+ get_environment,
25
+ get_region,
26
+ get_credential_alias,
27
+ get_trace_type,
28
+ get_trace_name,
29
+ get_parent_transaction_id,
30
+ get_transaction_name,
31
+ get_retry_number
32
+ )
33
+
34
+ logger = logging.getLogger("revenium_middleware.perplexity")
35
+
36
+
37
+ class OperationType(str, Enum):
38
+ """Operation types for AI API calls."""
39
+ CHAT = "CHAT"
40
+ GENERATE = "GENERATE"
41
+ EMBED = "EMBED"
42
+ OTHER = "OTHER"
43
+
44
+
45
+ def get_stop_reason(finish_reason: Optional[str]) -> str:
46
+ """
47
+ Map Perplexity/OpenAI finish reasons to Revenium stop reasons.
48
+
49
+ Valid Revenium stop reasons: END, END_SEQUENCE, TIMEOUT, TOKEN_LIMIT,
50
+ COST_LIMIT, COMPLETION_LIMIT, ERROR, CANCELLED
51
+
52
+ Args:
53
+ finish_reason: Finish reason from API response
54
+
55
+ Returns:
56
+ Mapped stop reason string
57
+ """
58
+ if not finish_reason:
59
+ return "END"
60
+
61
+ reason_map = {
62
+ "stop": "END",
63
+ "length": "TOKEN_LIMIT",
64
+ "content_filter": "ERROR",
65
+ "tool_calls": "END_SEQUENCE",
66
+ "function_call": "END_SEQUENCE",
67
+ }
68
+
69
+ return reason_map.get(finish_reason.lower(), "END")
70
+
71
+
72
+ def detect_operation_type(response: Any) -> OperationType:
73
+ """
74
+ Detect the operation type from the response.
75
+
76
+ Args:
77
+ response: API response object
78
+
79
+ Returns:
80
+ OperationType enum value
81
+ """
82
+ # For Perplexity, it's primarily chat completions
83
+ if hasattr(response, 'choices') and response.choices:
84
+ choice = response.choices[0]
85
+ if hasattr(choice, 'message'):
86
+ # Check for tool calls
87
+ if hasattr(choice.message, 'tool_calls') and choice.message.tool_calls:
88
+ return OperationType.OTHER # Could be TOOL_CALL if we add it
89
+ return OperationType.CHAT
90
+
91
+ return OperationType.CHAT
92
+
93
+
94
+ def extract_token_usage(response: Any) -> Dict[str, int]:
95
+ """
96
+ Extract token usage from API response.
97
+
98
+ Args:
99
+ response: API response object
100
+
101
+ Returns:
102
+ Dictionary with token counts
103
+ """
104
+ usage = {
105
+ "prompt_tokens": 0,
106
+ "completion_tokens": 0,
107
+ "total_tokens": 0,
108
+ }
109
+
110
+ if hasattr(response, 'usage') and response.usage:
111
+ usage["prompt_tokens"] = getattr(response.usage, 'prompt_tokens', 0)
112
+ usage["completion_tokens"] = getattr(response.usage, 'completion_tokens', 0)
113
+ usage["total_tokens"] = getattr(response.usage, 'total_tokens', 0)
114
+
115
+ return usage
116
+
117
+
118
+ def build_trace_fields() -> Dict[str, Any]:
119
+ """
120
+ Build trace visualization fields from environment variables.
121
+
122
+ Returns:
123
+ Dictionary with trace fields
124
+ """
125
+ fields = {}
126
+
127
+ # Add optional trace fields if available
128
+ if env := get_environment():
129
+ fields["environment"] = env
130
+ if region := get_region():
131
+ fields["region"] = region
132
+ if alias := get_credential_alias():
133
+ fields["credential_alias"] = alias
134
+ if trace_type := get_trace_type():
135
+ fields["trace_type"] = trace_type
136
+ if trace_name := get_trace_name():
137
+ fields["trace_name"] = trace_name
138
+ if parent_id := get_parent_transaction_id():
139
+ fields["parent_transaction_id"] = parent_id
140
+ if txn_name := get_transaction_name():
141
+ fields["transaction_name"] = txn_name
142
+
143
+ # Always include retry number (defaults to 0)
144
+ fields["retry_number"] = get_retry_number()
145
+
146
+ return fields
147
+
148
+
149
+ def send_metering_data(
150
+ response: Any,
151
+ request_time_dt: datetime.datetime,
152
+ usage_metadata: Dict[str, Any],
153
+ model: str,
154
+ provider: Provider,
155
+ is_streaming: bool = False,
156
+ transaction_id: Optional[str] = None,
157
+ ) -> None:
158
+ """
159
+ Send metering data to Revenium asynchronously.
160
+
161
+ Args:
162
+ response: API response object
163
+ request_time_dt: Request start time
164
+ usage_metadata: User-provided metadata
165
+ model: Model name
166
+ provider: Provider enum
167
+ is_streaming: Whether this is a streaming response
168
+ transaction_id: Transaction ID for tracking
169
+ """
170
+ async def metering_call():
171
+ try:
172
+ response_time_dt = datetime.datetime.now(datetime.timezone.utc)
173
+ request_duration = (response_time_dt - request_time_dt).total_seconds() * 1000
174
+
175
+ # Generate transaction ID if not provided
176
+ if transaction_id is None:
177
+ txn_id = getattr(response, 'id', str(uuid.uuid4()))
178
+ else:
179
+ txn_id = transaction_id
180
+
181
+ # Extract token usage
182
+ token_usage = extract_token_usage(response)
183
+
184
+ # Detect operation type
185
+ operation_type = detect_operation_type(response)
186
+
187
+ # Get stop reason
188
+ stop_reason = "END"
189
+ if hasattr(response, 'choices') and response.choices:
190
+ finish_reason = getattr(response.choices[0], 'finish_reason', None)
191
+ stop_reason = get_stop_reason(finish_reason)
192
+
193
+ # Get provider metadata
194
+ provider_metadata = get_provider_metadata(provider)
195
+
196
+ # Build trace fields
197
+ trace_fields = build_trace_fields()
198
+
199
+ # Build completion args
200
+ completion_args = {
201
+ # Required fields
202
+ "model": model,
203
+ "provider": provider_metadata["provider"],
204
+ "input_token_count": token_usage["prompt_tokens"],
205
+ "output_token_count": token_usage["completion_tokens"],
206
+ "total_token_count": token_usage["total_tokens"],
207
+ "request_duration": int(request_duration),
208
+ "request_time": request_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ"),
209
+ "response_time": response_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ"),
210
+ "completion_start_time": response_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ"),
211
+ "transaction_id": txn_id,
212
+ "stop_reason": stop_reason,
213
+ "is_streamed": is_streaming,
214
+ "cost_type": "AI",
215
+ "operation_type": operation_type.value,
216
+ # Optional fields with defaults
217
+ "cache_creation_token_count": 0,
218
+ "cache_read_token_count": 0,
219
+ "reasoning_token_count": 0,
220
+ }
221
+
222
+ # Add optional fields from usage_metadata if they have values
223
+ if usage_metadata.get("trace_id"):
224
+ completion_args["trace_id"] = usage_metadata.get("trace_id")
225
+ if usage_metadata.get("task_type"):
226
+ completion_args["task_type"] = usage_metadata.get("task_type")
227
+ if usage_metadata.get("organization_id"):
228
+ completion_args["organization_id"] = usage_metadata.get("organization_id")
229
+ if usage_metadata.get("subscription_id"):
230
+ completion_args["subscription_id"] = usage_metadata.get("subscription_id")
231
+ if usage_metadata.get("product_id"):
232
+ completion_args["product_id"] = usage_metadata.get("product_id")
233
+ if usage_metadata.get("agent"):
234
+ completion_args["agent"] = usage_metadata.get("agent")
235
+ if usage_metadata.get("subscriber"):
236
+ completion_args["subscriber"] = usage_metadata.get("subscriber")
237
+
238
+ # Add custom metadata fields (service, step, service_name, etc.)
239
+ # These are additional fields that may be used for tracing/tracking
240
+ custom_fields = ["service", "step", "service_name"]
241
+ for field in custom_fields:
242
+ if usage_metadata.get(field):
243
+ completion_args[field] = usage_metadata.get(field)
244
+
245
+ # Add trace visualization fields from trace_fields
246
+ for key, value in trace_fields.items():
247
+ if value is not None:
248
+ completion_args[key] = value
249
+
250
+ # Send to Revenium
251
+ logger.debug(f"Sending metering data: {completion_args}")
252
+ result = client.ai.create_completion(**completion_args)
253
+ logger.debug(f"Metering call result: {result}")
254
+
255
+ except Exception as e:
256
+ if not shutdown_event.is_set():
257
+ logger.warning(f"Error in metering call: {str(e)}")
258
+
259
+ # Run async in background thread
260
+ thread = run_async_in_thread(metering_call())
261
+ logger.debug(f"Metering thread started: {thread}")
262
+
263
+
264
+ def handle_streaming_response(
265
+ stream: Iterator[Any],
266
+ request_time_dt: datetime.datetime,
267
+ usage_metadata: Dict[str, Any],
268
+ model: str,
269
+ provider: Provider,
270
+ transaction_id: str,
271
+ ) -> Iterator[Any]:
272
+ """
273
+ Wrap a streaming response to collect usage data.
274
+
275
+ Args:
276
+ stream: Original stream iterator
277
+ request_time_dt: Request start time
278
+ usage_metadata: User-provided metadata
279
+ model: Model name
280
+ provider: Provider enum
281
+ transaction_id: Transaction ID for tracking
282
+
283
+ Yields:
284
+ Stream chunks from the original stream
285
+ """
286
+ accumulated_response = None
287
+
288
+ try:
289
+ for chunk in stream:
290
+ # Accumulate the final chunk with usage data
291
+ if hasattr(chunk, 'usage') and chunk.usage:
292
+ accumulated_response = chunk
293
+ yield chunk
294
+ finally:
295
+ # Send metering data after stream completes
296
+ if accumulated_response:
297
+ send_metering_data(
298
+ accumulated_response,
299
+ request_time_dt,
300
+ usage_metadata,
301
+ model,
302
+ provider,
303
+ is_streaming=True,
304
+ transaction_id=transaction_id,
305
+ )
306
+
307
+
308
+ @wrapt.patch_function_wrapper('openai.resources.chat.completions', 'Completions.create')
309
+ def create_wrapper(wrapped, instance, args, kwargs):
310
+ """
311
+ Wrapper for openai.chat.completions.create to add Revenium metering.
312
+
313
+ This wrapper:
314
+ 1. Detects if the client is using Perplexity base URL
315
+ 2. Captures request timing and metadata
316
+ 3. Calls the original OpenAI method
317
+ 4. Sends usage data to Revenium asynchronously
318
+ 5. Returns the original response unchanged
319
+ """
320
+ logger.debug("Perplexity chat completion wrapper called")
321
+
322
+ # Extract usage_metadata from kwargs or extra_body
323
+ api_metadata = kwargs.pop("usage_metadata", {})
324
+
325
+ # Also check extra_body for usage_metadata (for backward compatibility)
326
+ extra_body = kwargs.get('extra_body', {})
327
+ if isinstance(extra_body, dict) and 'usage_metadata' in extra_body:
328
+ extra_metadata = extra_body.pop('usage_metadata', {})
329
+ # Merge with existing usage_metadata (kwargs takes precedence)
330
+ api_metadata = {**extra_metadata, **api_metadata}
331
+
332
+ # Merge with decorator metadata (API metadata takes precedence)
333
+ usage_metadata = merge_metadata(api_metadata)
334
+
335
+ # Detect provider
336
+ client_instance = getattr(instance, '_client', None)
337
+ base_url = getattr(client_instance, 'base_url', None) if client_instance else None
338
+ provider = detect_provider(client=client_instance, base_url=base_url)
339
+
340
+ # Get model from kwargs
341
+ model = kwargs.get('model', 'unknown')
342
+
343
+ # Check if streaming
344
+ is_streaming = kwargs.get('stream', False)
345
+
346
+ # Record request time
347
+ request_time_dt = datetime.datetime.now(datetime.timezone.utc)
348
+
349
+ # Generate transaction ID using timestamp for consistency
350
+ transaction_id = f"perplexity-{request_time_dt.timestamp()}"
351
+
352
+ # Call original method
353
+ logger.debug(f"Calling original create with model: {model}, streaming: {is_streaming}")
354
+ response = wrapped(*args, **kwargs)
355
+
356
+ # Handle response based on streaming
357
+ if is_streaming:
358
+ # Wrap the stream to collect usage data
359
+ return handle_streaming_response(
360
+ response,
361
+ request_time_dt,
362
+ usage_metadata,
363
+ model,
364
+ provider,
365
+ transaction_id,
366
+ )
367
+ else:
368
+ # Send metering data for non-streaming response
369
+ send_metering_data(
370
+ response,
371
+ request_time_dt,
372
+ usage_metadata,
373
+ model,
374
+ provider,
375
+ is_streaming=False,
376
+ transaction_id=transaction_id,
377
+ )
378
+ return response
379
+