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,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for validating usage metadata in Revenium LiteLLM middleware.
|
|
3
|
+
|
|
4
|
+
This module provides type-safe validation for metadata fields using Pydantic.
|
|
5
|
+
It helps catch errors early in development and provides IDE autocomplete support.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from revenium_middleware.litellm.client import UsageMetadata
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Valid metadata
|
|
11
|
+
>>> metadata = UsageMetadata(
|
|
12
|
+
... organization_id="AcmeCorp",
|
|
13
|
+
... subscription_id="82764738",
|
|
14
|
+
... product_id="Platinum",
|
|
15
|
+
... trace_id="abc-123",
|
|
16
|
+
... agent="Lead Analyst",
|
|
17
|
+
... task_type="research"
|
|
18
|
+
... )
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Type errors caught immediately
|
|
21
|
+
>>> try:
|
|
22
|
+
... bad_metadata = UsageMetadata(organization_id=12345) # Wrong type
|
|
23
|
+
... except ValidationError as e:
|
|
24
|
+
... print(e)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
|
29
|
+
from typing import Optional, Dict, Any
|
|
30
|
+
PYDANTIC_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
# Pydantic is optional - provide fallback
|
|
33
|
+
PYDANTIC_AVAILABLE = False
|
|
34
|
+
BaseModel = object # type: ignore
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if PYDANTIC_AVAILABLE:
|
|
38
|
+
class SubscriberCredential(BaseModel):
|
|
39
|
+
"""
|
|
40
|
+
Credential information for a subscriber.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
name: An alias for an API key used by one or more users
|
|
44
|
+
value: The key value associated with the subscriber (e.g., an API key)
|
|
45
|
+
"""
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
|
|
48
|
+
name: Optional[str] = Field(
|
|
49
|
+
None,
|
|
50
|
+
description="An alias for an API key used by one or more users"
|
|
51
|
+
)
|
|
52
|
+
value: Optional[str] = Field(
|
|
53
|
+
None,
|
|
54
|
+
description="The key value associated with the subscriber (e.g., an API key)"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Subscriber(BaseModel):
|
|
59
|
+
"""
|
|
60
|
+
Subscriber information for tracking individual users.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
id: The ID of the subscriber from non-Revenium systems
|
|
64
|
+
email: The email address of the subscriber
|
|
65
|
+
credential: Credential information (API key, etc.)
|
|
66
|
+
"""
|
|
67
|
+
model_config = ConfigDict(extra="forbid")
|
|
68
|
+
|
|
69
|
+
id: Optional[str] = Field(
|
|
70
|
+
None,
|
|
71
|
+
description="The ID of the subscriber from non-Revenium systems"
|
|
72
|
+
)
|
|
73
|
+
email: Optional[str] = Field(
|
|
74
|
+
None,
|
|
75
|
+
description="The email address of the subscriber"
|
|
76
|
+
)
|
|
77
|
+
credential: Optional[SubscriberCredential] = Field(
|
|
78
|
+
None,
|
|
79
|
+
description="Credential information for the subscriber"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class UsageMetadata(BaseModel):
|
|
84
|
+
"""
|
|
85
|
+
Complete usage metadata for Revenium metering.
|
|
86
|
+
|
|
87
|
+
All fields are optional. Adding them enables more detailed reporting
|
|
88
|
+
and analytics in Revenium.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
trace_id: Unique identifier for a conversation or session
|
|
92
|
+
task_type: Classification of the AI operation by type of work
|
|
93
|
+
subscriber: Object containing subscriber information
|
|
94
|
+
organization_id: Customer or department ID from non-Revenium systems
|
|
95
|
+
subscription_id: Reference to a billing plan in non-Revenium systems
|
|
96
|
+
product_id: Your product or feature making the AI call
|
|
97
|
+
agent: Identifier for the specific AI agent
|
|
98
|
+
response_quality_score: The quality of the AI response (0.0 to 1.0)
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> metadata = UsageMetadata(
|
|
102
|
+
... organization_id="AcmeCorp",
|
|
103
|
+
... subscription_id="82764738",
|
|
104
|
+
... product_id="Platinum",
|
|
105
|
+
... trace_id="abc-123",
|
|
106
|
+
... agent="Lead Analyst",
|
|
107
|
+
... task_type="market_research",
|
|
108
|
+
... response_quality_score=0.95
|
|
109
|
+
... )
|
|
110
|
+
>>> metadata_dict = metadata.model_dump(exclude_none=True)
|
|
111
|
+
"""
|
|
112
|
+
model_config = ConfigDict(extra="allow") # Allow additional custom fields
|
|
113
|
+
|
|
114
|
+
trace_id: Optional[str] = Field(
|
|
115
|
+
None,
|
|
116
|
+
description="Unique identifier for a conversation or session"
|
|
117
|
+
)
|
|
118
|
+
task_type: Optional[str] = Field(
|
|
119
|
+
None,
|
|
120
|
+
description="Classification of the AI operation by type of work"
|
|
121
|
+
)
|
|
122
|
+
subscriber: Optional[Subscriber] = Field(
|
|
123
|
+
None,
|
|
124
|
+
description="Object containing subscriber information"
|
|
125
|
+
)
|
|
126
|
+
organization_id: Optional[str] = Field(
|
|
127
|
+
None,
|
|
128
|
+
description="Customer or department ID from non-Revenium systems"
|
|
129
|
+
)
|
|
130
|
+
subscription_id: Optional[str] = Field(
|
|
131
|
+
None,
|
|
132
|
+
description="Reference to a billing plan in non-Revenium systems"
|
|
133
|
+
)
|
|
134
|
+
product_id: Optional[str] = Field(
|
|
135
|
+
None,
|
|
136
|
+
description="Your product or feature making the AI call"
|
|
137
|
+
)
|
|
138
|
+
agent: Optional[str] = Field(
|
|
139
|
+
None,
|
|
140
|
+
description="Identifier for the specific AI agent"
|
|
141
|
+
)
|
|
142
|
+
response_quality_score: Optional[float] = Field(
|
|
143
|
+
None,
|
|
144
|
+
ge=0.0,
|
|
145
|
+
le=1.0,
|
|
146
|
+
description="The quality of the AI response (0.0 to 1.0)"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@field_validator('response_quality_score')
|
|
150
|
+
@classmethod
|
|
151
|
+
def validate_quality_score(cls, v: Optional[float]) -> Optional[float]:
|
|
152
|
+
"""Validate that quality score is between 0 and 1."""
|
|
153
|
+
if v is not None and (v < 0.0 or v > 1.0):
|
|
154
|
+
raise ValueError('response_quality_score must be between 0.0 and 1.0')
|
|
155
|
+
return v
|
|
156
|
+
|
|
157
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Convert to dictionary, excluding None values.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dict with only non-None values
|
|
163
|
+
"""
|
|
164
|
+
return self.model_dump(exclude_none=True)
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
# Fallback when Pydantic is not installed
|
|
168
|
+
class UsageMetadata: # type: ignore
|
|
169
|
+
"""
|
|
170
|
+
Fallback UsageMetadata class when Pydantic is not installed.
|
|
171
|
+
|
|
172
|
+
This provides basic functionality without validation. For full
|
|
173
|
+
type safety and validation, install pydantic:
|
|
174
|
+
|
|
175
|
+
pip install "revenium-python-sdk[litellm]"
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(self, **kwargs):
|
|
179
|
+
"""Initialize with any keyword arguments."""
|
|
180
|
+
self.__dict__.update(kwargs)
|
|
181
|
+
|
|
182
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
183
|
+
"""Convert to dictionary, excluding None values."""
|
|
184
|
+
return {k: v for k, v in self.__dict__.items() if v is not None}
|
|
185
|
+
|
|
186
|
+
class Subscriber: # type: ignore
|
|
187
|
+
"""Fallback Subscriber class when Pydantic is not installed."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, **kwargs):
|
|
190
|
+
"""Initialize with any keyword arguments."""
|
|
191
|
+
self.__dict__.update(kwargs)
|
|
192
|
+
|
|
193
|
+
class SubscriberCredential: # type: ignore
|
|
194
|
+
"""Fallback SubscriberCredential class when Pydantic is not installed."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, **kwargs):
|
|
197
|
+
"""Initialize with any keyword arguments."""
|
|
198
|
+
self.__dict__.update(kwargs)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
__all__ = [
|
|
202
|
+
'UsageMetadata',
|
|
203
|
+
'Subscriber',
|
|
204
|
+
'SubscriberCredential',
|
|
205
|
+
'PYDANTIC_AVAILABLE'
|
|
206
|
+
]
|
|
207
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Revenium LiteLLM Proxy Middleware
|
|
3
|
+
|
|
4
|
+
When you install and import this library, it will automatically hook
|
|
5
|
+
LiteLLM proxy requests using a custom logger, and log token usage after
|
|
6
|
+
each request. You can customize or extend this logging logic later
|
|
7
|
+
to add user or organization metadata for metering purposes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import litellm # noqa: F401
|
|
16
|
+
from .middleware import MiddlewareHandler, proxy_handler_instance
|
|
17
|
+
except ImportError:
|
|
18
|
+
logger.debug("LiteLLM SDK (litellm) not available, proxy middleware not loaded")
|
|
19
|
+
MiddlewareHandler = None # type: ignore
|
|
20
|
+
proxy_handler_instance = None # type: ignore
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"MiddlewareHandler",
|
|
24
|
+
"proxy_handler_instance",
|
|
25
|
+
]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from litellm.integrations.custom_logger import CustomLogger
|
|
2
|
+
from revenium_middleware import client, run_async_in_thread
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _extract_organization_name(headers, metadata):
|
|
9
|
+
"""
|
|
10
|
+
Extract organization name with fallback priority.
|
|
11
|
+
|
|
12
|
+
Priority: x-revenium-organization-name > x-revenium-organization-id > user_api_key_team_alias
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
headers: Request headers dictionary
|
|
16
|
+
metadata: Request metadata dictionary
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Organization name string or None
|
|
20
|
+
"""
|
|
21
|
+
organization_name = headers.get("x-revenium-organization-name")
|
|
22
|
+
if organization_name is None:
|
|
23
|
+
organization_name = headers.get("x-revenium-organization-id")
|
|
24
|
+
if organization_name is None:
|
|
25
|
+
organization_name = metadata.get('user_api_key_team_alias', '') or None
|
|
26
|
+
return organization_name
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _extract_product_name(headers):
|
|
30
|
+
"""
|
|
31
|
+
Extract product name with fallback priority.
|
|
32
|
+
|
|
33
|
+
Priority: x-revenium-product-name > x-revenium-product-id
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
headers: Request headers dictionary
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Product name string or None
|
|
40
|
+
"""
|
|
41
|
+
product_name = headers.get("x-revenium-product-name")
|
|
42
|
+
if product_name is None:
|
|
43
|
+
product_name = headers.get("x-revenium-product-id")
|
|
44
|
+
return product_name
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MiddlewareHandler(CustomLogger):
|
|
48
|
+
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
|
49
|
+
# log: key, user, model, prompt, response, tokens, cost
|
|
50
|
+
# Access kwargs passed to litellm.completion()
|
|
51
|
+
# pprint.pprint(kwargs['litellm_params'])
|
|
52
|
+
|
|
53
|
+
model = kwargs.get("model", None)
|
|
54
|
+
|
|
55
|
+
# Access litellm_params passed to litellm.completion(), example access `metadata`
|
|
56
|
+
litellm_params = kwargs.get("litellm_params", {})
|
|
57
|
+
metadata = litellm_params.get("metadata", {}) # headers passed to LiteLLM proxy, can be found here
|
|
58
|
+
headers = metadata.get("headers", {})
|
|
59
|
+
|
|
60
|
+
response = response_obj
|
|
61
|
+
# tokens used in response
|
|
62
|
+
usage = response_obj["usage"]
|
|
63
|
+
|
|
64
|
+
# Create subscriber object from metadata and headers
|
|
65
|
+
subscriber = {}
|
|
66
|
+
|
|
67
|
+
# Extract subscriber information from metadata and headers
|
|
68
|
+
subscriber_id = metadata.get('x-revenium-subscriber-id', '') or headers.get("x-revenium-subscriber-id")
|
|
69
|
+
subscriber_email = metadata.get('user_api_key_user_email', '')
|
|
70
|
+
credential_name = metadata.get('user_api_key_alias', '')
|
|
71
|
+
credential_value = metadata.get('user_api_key_alias', '')
|
|
72
|
+
|
|
73
|
+
if subscriber_id:
|
|
74
|
+
subscriber["id"] = subscriber_id
|
|
75
|
+
if subscriber_email:
|
|
76
|
+
subscriber["email"] = subscriber_email
|
|
77
|
+
if credential_name or credential_value:
|
|
78
|
+
subscriber["credential"] = {
|
|
79
|
+
"name": credential_name,
|
|
80
|
+
"value": credential_value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Extract organization and product names using helper functions
|
|
84
|
+
organization_name = _extract_organization_name(headers, metadata)
|
|
85
|
+
product_name = _extract_product_name(headers)
|
|
86
|
+
|
|
87
|
+
completion_args = {
|
|
88
|
+
"cache_creation_token_count": 0,
|
|
89
|
+
"cache_read_token_count": 0,
|
|
90
|
+
"input_token_cost": None,
|
|
91
|
+
"output_token_cost": None,
|
|
92
|
+
"total_cost": None,
|
|
93
|
+
"output_token_count": usage.completion_tokens,
|
|
94
|
+
"cost_type": "AI",
|
|
95
|
+
"model": model,
|
|
96
|
+
"input_token_count": usage.prompt_tokens,
|
|
97
|
+
"provider": "LITELLM",
|
|
98
|
+
"model_source": "LITELLM",
|
|
99
|
+
"reasoning_token_count": 0,
|
|
100
|
+
"request_time": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
101
|
+
"response_time": end_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
102
|
+
"completion_start_time": end_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
103
|
+
"request_duration": (end_time - start_time).total_seconds() * 1000, # Convert to milliseconds
|
|
104
|
+
"time_to_first_token": (end_time - start_time).total_seconds() * 1000,
|
|
105
|
+
"stop_reason": "END",
|
|
106
|
+
"total_token_count": usage.total_tokens,
|
|
107
|
+
"transaction_id": response.id,
|
|
108
|
+
"trace_id": headers.get("x-revenium-trace-id"),
|
|
109
|
+
"task_type": headers.get("x-revenium-task-type"),
|
|
110
|
+
"subscriber": subscriber if subscriber else None,
|
|
111
|
+
"organization_name": organization_name,
|
|
112
|
+
"subscription_id": headers.get("x-revenium-subscription-id"),
|
|
113
|
+
"product_name": product_name,
|
|
114
|
+
"agent": headers.get("x-revenium-agent"),
|
|
115
|
+
"response_quality_score": headers.get("x-revenium-response-quality-score"),
|
|
116
|
+
"is_streamed": metadata.get('hidden_params', {}).get('optional_params', {}).get('stream', False),
|
|
117
|
+
"operation_type": "CHAT",
|
|
118
|
+
"mediation_latency": metadata.get('hidden_params', {}).get('litellm_overhead_time_ms', 0),
|
|
119
|
+
"middleware_source": "PROXY",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.debug("Calling client.ai.create_completion with args: %s", completion_args)
|
|
123
|
+
result = client.ai.create_completion(**completion_args)
|
|
124
|
+
logger.debug("Result from create_completion: %s", result)
|
|
125
|
+
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
|
129
|
+
# log: key, user, model, prompt, error, tokens, cost
|
|
130
|
+
# Access kwargs passed to litellm.completion()
|
|
131
|
+
# pprint.pprint(kwargs['litellm_params'])
|
|
132
|
+
|
|
133
|
+
model = kwargs.get("model", None)
|
|
134
|
+
|
|
135
|
+
# Access litellm_params passed to litellm.completion(), example access `metadata`
|
|
136
|
+
litellm_params = kwargs.get("litellm_params", {})
|
|
137
|
+
metadata = litellm_params.get("metadata", {}) # headers passed to LiteLLM proxy, can be found here
|
|
138
|
+
headers = metadata.get("headers", {})
|
|
139
|
+
|
|
140
|
+
# For failures, we may not have usage information
|
|
141
|
+
usage = getattr(response_obj, "usage", {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0})
|
|
142
|
+
if isinstance(usage, dict) is False:
|
|
143
|
+
usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
|
144
|
+
|
|
145
|
+
error_message = str(response_obj)
|
|
146
|
+
error_type = type(response_obj).__name__
|
|
147
|
+
|
|
148
|
+
# Create subscriber object from metadata and headers
|
|
149
|
+
subscriber = {}
|
|
150
|
+
|
|
151
|
+
# Extract subscriber information from metadata and headers
|
|
152
|
+
subscriber_id = metadata.get('x-revenium-subscriber-id', '') or headers.get("x-revenium-subscriber-id")
|
|
153
|
+
subscriber_email = metadata.get('user_api_key_user_email', '')
|
|
154
|
+
credential_name = metadata.get('user_api_key_alias', '')
|
|
155
|
+
credential_value = metadata.get('user_api_key_hash', '')
|
|
156
|
+
|
|
157
|
+
if subscriber_id:
|
|
158
|
+
subscriber["id"] = subscriber_id
|
|
159
|
+
if subscriber_email:
|
|
160
|
+
subscriber["email"] = subscriber_email
|
|
161
|
+
if credential_name or credential_value:
|
|
162
|
+
subscriber["credential"] = {
|
|
163
|
+
"name": credential_name,
|
|
164
|
+
"value": credential_value
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Extract organization and product names using helper functions
|
|
168
|
+
organization_name = _extract_organization_name(headers, metadata)
|
|
169
|
+
product_name = _extract_product_name(headers)
|
|
170
|
+
|
|
171
|
+
completion_args = {
|
|
172
|
+
"cache_creation_token_count": 0,
|
|
173
|
+
"cache_read_token_count": 0,
|
|
174
|
+
"input_token_cost": None,
|
|
175
|
+
"output_token_cost": None,
|
|
176
|
+
"total_cost": None,
|
|
177
|
+
"output_token_count": usage.get("completion_tokens", 0),
|
|
178
|
+
"cost_type": "AI",
|
|
179
|
+
"model": model,
|
|
180
|
+
"input_token_count": usage.get("prompt_tokens", 0),
|
|
181
|
+
"provider": "LITELLM",
|
|
182
|
+
"model_source": "LITELLM",
|
|
183
|
+
"reasoning_token_count": 0,
|
|
184
|
+
"request_time": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
185
|
+
"response_time": end_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
186
|
+
"completion_start_time": end_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
187
|
+
"request_duration": (end_time - start_time).total_seconds() * 1000, # Convert to milliseconds
|
|
188
|
+
"time_to_first_token": (end_time - start_time).total_seconds() * 1000,
|
|
189
|
+
# Time to first token in milliseconds
|
|
190
|
+
"stop_reason": "ERROR",
|
|
191
|
+
"total_token_count": usage.get("total_tokens", 0),
|
|
192
|
+
"transaction_id": getattr(response_obj, "id", "error-no-id"),
|
|
193
|
+
"trace_id": headers.get("x-revenium-trace-id"),
|
|
194
|
+
"task_type": headers.get("x-revenium-task-type"),
|
|
195
|
+
"subscriber": subscriber if subscriber else None,
|
|
196
|
+
"organization_name": organization_name,
|
|
197
|
+
"subscription_id": headers.get("x-revenium-subscription-id"),
|
|
198
|
+
"product_name": product_name,
|
|
199
|
+
"agent": headers.get("x-revenium-agent"),
|
|
200
|
+
"response_quality_score": headers.get("x-revenium-response-quality-score"),
|
|
201
|
+
"is_streamed": metadata.get('hidden_params', {}).get('optional_params', {}).get('stream', False),
|
|
202
|
+
"operation_type": "CHAT",
|
|
203
|
+
"middleware_source": "PROXY"
|
|
204
|
+
# "error_message": error_message,
|
|
205
|
+
# "error_type": error_type,
|
|
206
|
+
# "mediation_latency": metadata['hidden_params']['litellm_overhead_time_ms'],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
logger.debug("Calling client.ai.create_completion with args (failure): %s", completion_args)
|
|
210
|
+
try:
|
|
211
|
+
result = client.ai.create_completion(**completion_args)
|
|
212
|
+
logger.debug("Result from create_completion (failure): %s", result)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error("Error logging failure event: %s", e)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
proxy_handler_instance = MiddlewareHandler()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Revenium Middleware for Ollama Python SDK.
|
|
3
|
+
|
|
4
|
+
When you install and import this library, it will automatically hook
|
|
5
|
+
ollama.chat, ollama.generate, and ollama.embed using wrapt, and log token usage after
|
|
6
|
+
each request. You can customize or extend this logging logic later
|
|
7
|
+
to add user or organization metadata for metering purposes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Conditionally import middleware (requires ollama SDK)
|
|
15
|
+
try:
|
|
16
|
+
import ollama as _ollama # noqa: F401
|
|
17
|
+
from .middleware import chat_wrapper, generate_wrapper, embed_wrapper
|
|
18
|
+
except ImportError:
|
|
19
|
+
logger.debug("Ollama SDK (ollama) not available, middleware not loaded")
|
|
20
|
+
chat_wrapper = None # type: ignore
|
|
21
|
+
generate_wrapper = None # type: ignore
|
|
22
|
+
embed_wrapper = None # type: ignore
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"chat_wrapper",
|
|
26
|
+
"generate_wrapper",
|
|
27
|
+
"embed_wrapper",
|
|
28
|
+
]
|