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