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,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
|
+
|