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,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Azure OpenAI configuration management.
|
|
3
|
+
|
|
4
|
+
This module handles Azure-specific environment variables and configuration
|
|
5
|
+
according to F2 requirements. Configuration is loaded lazily only when
|
|
6
|
+
Azure mode is detected to avoid performance impact for non-Azure users.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AzureConfig:
|
|
17
|
+
"""
|
|
18
|
+
Azure OpenAI configuration manager.
|
|
19
|
+
|
|
20
|
+
Handles Azure-specific environment variables and provides
|
|
21
|
+
configuration validation and header generation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
"""Initialize Azure configuration from environment variables."""
|
|
26
|
+
self.endpoint = os.environ.get('AZURE_OPENAI_ENDPOINT')
|
|
27
|
+
self.deployment = os.environ.get('AZURE_OPENAI_DEPLOYMENT')
|
|
28
|
+
self.api_version = os.environ.get('AZURE_OPENAI_API_VERSION', '2024-10-21')
|
|
29
|
+
self.api_key = os.environ.get('AZURE_OPENAI_API_KEY')
|
|
30
|
+
|
|
31
|
+
# Future AAD support
|
|
32
|
+
self.tenant_id = os.environ.get('AZURE_OPENAI_TENANT_ID')
|
|
33
|
+
self.resource_group = os.environ.get('AZURE_OPENAI_RESOURCE_GROUP')
|
|
34
|
+
|
|
35
|
+
logger.debug(f"Azure config initialized - endpoint: {self.endpoint}, "
|
|
36
|
+
f"deployment: {self.deployment}, api_version: {self.api_version}")
|
|
37
|
+
|
|
38
|
+
def is_valid(self) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Check if the minimum required configuration is present.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if configuration is valid for Azure OpenAI usage
|
|
44
|
+
"""
|
|
45
|
+
# Minimum requirement is endpoint
|
|
46
|
+
is_valid = bool(self.endpoint)
|
|
47
|
+
|
|
48
|
+
if not is_valid:
|
|
49
|
+
logger.debug("Azure config invalid - missing AZURE_OPENAI_ENDPOINT")
|
|
50
|
+
|
|
51
|
+
return is_valid
|
|
52
|
+
|
|
53
|
+
def get_headers(self, api_key: Optional[str] = None) -> Dict[str, str]:
|
|
54
|
+
"""
|
|
55
|
+
Generate appropriate headers for Azure OpenAI requests.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
api_key: Optional API key override
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary of headers for Azure OpenAI requests
|
|
62
|
+
"""
|
|
63
|
+
headers = {}
|
|
64
|
+
|
|
65
|
+
# Use provided key or fall back to config
|
|
66
|
+
key_to_use = api_key or self.api_key
|
|
67
|
+
|
|
68
|
+
if key_to_use:
|
|
69
|
+
# Azure OpenAI uses 'api-key' header instead of 'Authorization'
|
|
70
|
+
headers['api-key'] = key_to_use
|
|
71
|
+
logger.debug("Added api-key header for Azure authentication")
|
|
72
|
+
else:
|
|
73
|
+
logger.warning("No API key available for Azure OpenAI authentication")
|
|
74
|
+
|
|
75
|
+
return headers
|
|
76
|
+
|
|
77
|
+
def get_base_url(self) -> Optional[str]:
|
|
78
|
+
"""
|
|
79
|
+
Get the base URL for Azure OpenAI API calls.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Base URL or None if not configured
|
|
83
|
+
"""
|
|
84
|
+
if self.endpoint:
|
|
85
|
+
# Ensure endpoint ends with /openai for API calls
|
|
86
|
+
base_url = self.endpoint.rstrip('/')
|
|
87
|
+
if not base_url.endswith('/openai'):
|
|
88
|
+
base_url += '/openai'
|
|
89
|
+
return base_url
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def get_deployment_url(self, endpoint_path: str) -> Optional[str]:
|
|
93
|
+
"""
|
|
94
|
+
Construct full deployment URL for Azure OpenAI API calls.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
endpoint_path: API endpoint path (e.g., '/chat/completions')
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Full URL with deployment and API version
|
|
101
|
+
"""
|
|
102
|
+
if not self.is_valid() or not self.deployment:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
base_url = self.get_base_url()
|
|
106
|
+
if not base_url:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
# Construct: https://resource.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version=2024-10-21
|
|
110
|
+
url = f"{base_url}/deployments/{self.deployment}{endpoint_path}?api-version={self.api_version}"
|
|
111
|
+
logger.debug(f"Constructed Azure deployment URL: {url}")
|
|
112
|
+
return url
|
|
113
|
+
|
|
114
|
+
def validate_deployment(self) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Validate that deployment name is configured.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if deployment is configured
|
|
120
|
+
"""
|
|
121
|
+
if not self.deployment:
|
|
122
|
+
logger.warning("AZURE_OPENAI_DEPLOYMENT not configured - this may cause API calls to fail")
|
|
123
|
+
return False
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
Convert configuration to dictionary (excluding sensitive data).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Configuration dictionary for logging/debugging
|
|
132
|
+
"""
|
|
133
|
+
return {
|
|
134
|
+
'endpoint': self.endpoint,
|
|
135
|
+
'deployment': self.deployment,
|
|
136
|
+
'api_version': self.api_version,
|
|
137
|
+
'tenant_id': self.tenant_id,
|
|
138
|
+
'resource_group': self.resource_group,
|
|
139
|
+
'has_api_key': bool(self.api_key),
|
|
140
|
+
'is_valid': self.is_valid()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Global configuration instance - lazy loaded
|
|
145
|
+
_azure_config: Optional[AzureConfig] = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_azure_config() -> AzureConfig:
|
|
149
|
+
"""
|
|
150
|
+
Get or create Azure configuration instance.
|
|
151
|
+
|
|
152
|
+
Lazy loading ensures non-Azure users don't pay any performance cost.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
AzureConfig instance
|
|
156
|
+
"""
|
|
157
|
+
global _azure_config
|
|
158
|
+
|
|
159
|
+
if _azure_config is None:
|
|
160
|
+
_azure_config = AzureConfig()
|
|
161
|
+
logger.debug("Azure configuration loaded")
|
|
162
|
+
|
|
163
|
+
return _azure_config
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def reset_azure_config():
|
|
167
|
+
"""Reset Azure configuration cache. Useful for testing."""
|
|
168
|
+
global _azure_config
|
|
169
|
+
_azure_config = None
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Azure OpenAI model name resolution.
|
|
3
|
+
|
|
4
|
+
This module maps Azure deployment names to LiteLLM-compatible model names
|
|
5
|
+
for accurate pricing. Uses heuristic pattern matching with optional API fallback.
|
|
6
|
+
|
|
7
|
+
The resolution is fire-and-forget - failures cannot break primary AI calls.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional, Dict, Any
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from .config import Config
|
|
17
|
+
from .exceptions import ModelResolutionError, handle_exception_safely
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
20
|
+
|
|
21
|
+
# Global model name cache - deployment_name -> litellm_model_name
|
|
22
|
+
_azure_model_cache: Dict[str, str] = {}
|
|
23
|
+
_cache_lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_azure_model_name(deployment_name: str, base_url: Optional[str] = None,
|
|
27
|
+
headers: Optional[Dict[str, str]] = None,
|
|
28
|
+
use_api_fallback: bool = True) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Resolve Azure deployment name to LiteLLM-compatible model name.
|
|
31
|
+
|
|
32
|
+
This function is fire-and-forget - it will never raise exceptions that
|
|
33
|
+
could break the primary AI call.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
deployment_name: Azure deployment name from API response
|
|
37
|
+
base_url: Azure endpoint URL (for API fallback)
|
|
38
|
+
headers: Request headers (for API fallback)
|
|
39
|
+
use_api_fallback: Whether to attempt API resolution after heuristics
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
LiteLLM model name or original deployment_name if no match found
|
|
43
|
+
"""
|
|
44
|
+
logger.debug(f"Resolving Azure model name for deployment: {deployment_name}")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# 1. Check in-memory cache first
|
|
48
|
+
with _cache_lock:
|
|
49
|
+
if deployment_name in _azure_model_cache:
|
|
50
|
+
cached_name = _azure_model_cache[deployment_name]
|
|
51
|
+
logger.debug(f"Found cached mapping: {deployment_name} -> {cached_name}")
|
|
52
|
+
return cached_name
|
|
53
|
+
|
|
54
|
+
# 2. Try heuristic matching first (fast, reliable for common cases)
|
|
55
|
+
heuristic_name = _heuristic_model_mapping(deployment_name)
|
|
56
|
+
if heuristic_name:
|
|
57
|
+
logger.debug(f"Heuristic mapping successful: {deployment_name} -> {heuristic_name}")
|
|
58
|
+
with _cache_lock:
|
|
59
|
+
_azure_model_cache[deployment_name] = heuristic_name
|
|
60
|
+
return heuristic_name
|
|
61
|
+
|
|
62
|
+
# 3. Log warning - heuristics failed
|
|
63
|
+
logger.warning(f"No heuristic match found for Azure deployment '{deployment_name}'. "
|
|
64
|
+
f"This may result in pricing lookup failures on Revenium backend.")
|
|
65
|
+
|
|
66
|
+
# 4. Try Azure API fallback if enabled
|
|
67
|
+
if use_api_fallback and base_url and headers:
|
|
68
|
+
logger.debug(f"Attempting Azure API resolution for deployment: {deployment_name}")
|
|
69
|
+
_async_resolve_model_name(deployment_name, base_url, headers)
|
|
70
|
+
|
|
71
|
+
# 5. Return deployment name as fallback
|
|
72
|
+
logger.warning(f"Using deployment name '{deployment_name}' as model name. "
|
|
73
|
+
f"Verify this matches LiteLLM pricing tables to ensure accurate cost calculation.")
|
|
74
|
+
return deployment_name
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
# CRITICAL: Never let this break the main AI call
|
|
78
|
+
logger.warning(f"Azure model resolution failed for '{deployment_name}': {str(e)}. "
|
|
79
|
+
f"Using deployment name as fallback.")
|
|
80
|
+
return deployment_name
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _heuristic_model_mapping(deployment_name: str) -> Optional[str]:
|
|
84
|
+
"""
|
|
85
|
+
Fast pattern matching for common Azure deployment naming patterns.
|
|
86
|
+
|
|
87
|
+
Based on LiteLLM model names: azure_ai/gpt-4, azure_ai/gpt-35-turbo, etc.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
deployment_name: Azure deployment name
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
LiteLLM model name if pattern matched, None otherwise
|
|
94
|
+
"""
|
|
95
|
+
# Normalize deployment name for pattern matching
|
|
96
|
+
name_lower = deployment_name.lower().replace('_', '-').replace('.', '-')
|
|
97
|
+
|
|
98
|
+
# GPT-4o family patterns (highest priority - newest models)
|
|
99
|
+
if re.search(r'gpt-?4o', name_lower) or re.search(r'o4', name_lower):
|
|
100
|
+
if 'mini' in name_lower:
|
|
101
|
+
return 'gpt-4o-mini'
|
|
102
|
+
return 'gpt-4o'
|
|
103
|
+
|
|
104
|
+
# GPT-4 family patterns
|
|
105
|
+
if re.search(r'gpt-?4', name_lower):
|
|
106
|
+
# Check for specific variants first
|
|
107
|
+
if any(variant in name_lower for variant in ['turbo', '1106', '0125', '0613']):
|
|
108
|
+
return 'gpt-4-turbo'
|
|
109
|
+
if any(variant in name_lower for variant in ['vision', 'v', 'preview']):
|
|
110
|
+
return 'gpt-4-vision-preview'
|
|
111
|
+
if '32k' in name_lower:
|
|
112
|
+
return 'gpt-4-32k'
|
|
113
|
+
# Default GPT-4
|
|
114
|
+
return 'gpt-4'
|
|
115
|
+
|
|
116
|
+
# GPT-3.5 family patterns
|
|
117
|
+
if any(pattern in name_lower for pattern in ['gpt-35', 'gpt-3-5', 'gpt3-5', 'gpt35']):
|
|
118
|
+
if '16k' in name_lower:
|
|
119
|
+
return 'gpt-3.5-turbo-16k'
|
|
120
|
+
if 'instruct' in name_lower:
|
|
121
|
+
return 'gpt-3.5-turbo-instruct'
|
|
122
|
+
return 'gpt-3.5-turbo'
|
|
123
|
+
|
|
124
|
+
# Embedding model patterns
|
|
125
|
+
if 'embedding' in name_lower or 'embed' in name_lower:
|
|
126
|
+
if 'ada-002' in name_lower or 'ada002' in name_lower:
|
|
127
|
+
return 'text-embedding-ada-002'
|
|
128
|
+
if '3-large' in name_lower or 'large' in name_lower:
|
|
129
|
+
return 'text-embedding-3-large'
|
|
130
|
+
if '3-small' in name_lower or 'small' in name_lower:
|
|
131
|
+
return 'text-embedding-3-small'
|
|
132
|
+
|
|
133
|
+
# DALL-E patterns
|
|
134
|
+
if 'dall' in name_lower or 'dalle' in name_lower:
|
|
135
|
+
if '3' in name_lower:
|
|
136
|
+
return 'dall-e-3'
|
|
137
|
+
if '2' in name_lower:
|
|
138
|
+
return 'dall-e-2'
|
|
139
|
+
|
|
140
|
+
# Whisper patterns
|
|
141
|
+
if 'whisper' in name_lower:
|
|
142
|
+
return 'whisper-1'
|
|
143
|
+
|
|
144
|
+
# TTS patterns
|
|
145
|
+
if 'tts' in name_lower:
|
|
146
|
+
if 'hd' in name_lower:
|
|
147
|
+
return 'tts-1-hd'
|
|
148
|
+
return 'tts-1'
|
|
149
|
+
|
|
150
|
+
# No pattern matched
|
|
151
|
+
logger.debug(f"No heuristic pattern matched for deployment: {deployment_name}")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _async_resolve_model_name(deployment_name: str, base_url: str, headers: Dict[str, str]):
|
|
156
|
+
"""
|
|
157
|
+
Background Azure API resolution - never blocks main thread.
|
|
158
|
+
Updates cache if successful, silent failure otherwise.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
deployment_name: Azure deployment name
|
|
162
|
+
base_url: Azure endpoint URL
|
|
163
|
+
headers: Request headers for authentication
|
|
164
|
+
"""
|
|
165
|
+
def background_resolve():
|
|
166
|
+
try:
|
|
167
|
+
# Import here to avoid dependency issues for non-Azure users
|
|
168
|
+
import requests
|
|
169
|
+
|
|
170
|
+
# Construct Azure model info endpoint
|
|
171
|
+
# Format: https://resource.openai.azure.com/openai/deployments/{deployment}?api-version=2024-10-21
|
|
172
|
+
api_url = f"{base_url.rstrip('/')}/openai/deployments/{deployment_name}?api-version={Config.AZURE_API_VERSION_DEFAULT}"
|
|
173
|
+
|
|
174
|
+
logger.debug(f"Attempting Azure API model resolution: {api_url}")
|
|
175
|
+
|
|
176
|
+
# Make API call with timeout
|
|
177
|
+
response = requests.get(api_url, headers=headers, timeout=Config.AZURE_MODEL_RESOLUTION_TIMEOUT)
|
|
178
|
+
|
|
179
|
+
if response.status_code == 200:
|
|
180
|
+
model_info = response.json()
|
|
181
|
+
actual_model = model_info.get('id', deployment_name)
|
|
182
|
+
|
|
183
|
+
# Update cache
|
|
184
|
+
with _cache_lock:
|
|
185
|
+
_azure_model_cache[deployment_name] = actual_model
|
|
186
|
+
|
|
187
|
+
logger.debug(f"Azure API resolved: {deployment_name} -> {actual_model}")
|
|
188
|
+
else:
|
|
189
|
+
logger.debug(f"Azure API resolution failed for {deployment_name}: HTTP {response.status_code}")
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
# Silent failure - heuristic result already used
|
|
193
|
+
logger.debug(f"Background Azure API resolution failed for {deployment_name}: {str(e)}")
|
|
194
|
+
|
|
195
|
+
# Fire and forget - daemon thread won't block shutdown
|
|
196
|
+
thread = threading.Thread(target=background_resolve, daemon=True)
|
|
197
|
+
thread.start()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_model_cache_stats() -> Dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Get statistics about the model name cache.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dictionary with cache statistics
|
|
206
|
+
"""
|
|
207
|
+
with _cache_lock:
|
|
208
|
+
return {
|
|
209
|
+
'cache_size': len(_azure_model_cache),
|
|
210
|
+
'cached_models': dict(_azure_model_cache),
|
|
211
|
+
'cache_hit_rate': 'N/A' # Could be implemented with counters
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def clear_model_cache():
|
|
216
|
+
"""Clear the model name cache. Useful for testing."""
|
|
217
|
+
with _cache_lock:
|
|
218
|
+
_azure_model_cache.clear()
|
|
219
|
+
logger.debug("Azure model cache cleared")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration for Revenium OpenAI middleware.
|
|
3
|
+
|
|
4
|
+
Extends shared core config with OpenAI and Azure-specific settings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from revenium_middleware._core.config import ( # noqa: F401
|
|
8
|
+
Config as _CoreConfig,
|
|
9
|
+
SecurityConfig,
|
|
10
|
+
SummaryFormat,
|
|
11
|
+
get_config_value,
|
|
12
|
+
is_debug_enabled,
|
|
13
|
+
get_timeout_config as _core_get_timeout_config,
|
|
14
|
+
parse_print_summary_value,
|
|
15
|
+
get_print_summary_config,
|
|
16
|
+
get_team_id,
|
|
17
|
+
get_base_url,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Config(_CoreConfig):
|
|
22
|
+
"""OpenAI-specific configuration extending core config."""
|
|
23
|
+
|
|
24
|
+
# Azure API settings
|
|
25
|
+
AZURE_API_VERSION_DEFAULT: str = "2024-10-21"
|
|
26
|
+
AZURE_MODEL_RESOLUTION_TIMEOUT: float = 5.0
|
|
27
|
+
|
|
28
|
+
# Provider-specific environment variables
|
|
29
|
+
ENV_OPENAI_API_KEY: str = "OPENAI_API_KEY"
|
|
30
|
+
ENV_AZURE_OPENAI_ENDPOINT: str = "AZURE_OPENAI_ENDPOINT"
|
|
31
|
+
ENV_AZURE_OPENAI_API_KEY: str = "AZURE_OPENAI_API_KEY"
|
|
32
|
+
|
|
33
|
+
# Region environment variables
|
|
34
|
+
ENV_AWS_REGION: str = "AWS_REGION"
|
|
35
|
+
ENV_AWS_DEFAULT_REGION: str = "AWS_DEFAULT_REGION"
|
|
36
|
+
ENV_AZURE_REGION: str = "AZURE_REGION"
|
|
37
|
+
ENV_GCP_REGION: str = "GCP_REGION"
|
|
38
|
+
ENV_GOOGLE_CLOUD_REGION: str = "GOOGLE_CLOUD_REGION"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_timeout_config() -> dict:
|
|
42
|
+
"""Get all timeout-related configuration, including Azure."""
|
|
43
|
+
config = _core_get_timeout_config()
|
|
44
|
+
config['azure_model_resolution'] = Config.AZURE_MODEL_RESOLUTION_TIMEOUT
|
|
45
|
+
return config
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for Revenium middleware.
|
|
3
|
+
|
|
4
|
+
This module defines a hierarchy of exceptions that provide better error handling
|
|
5
|
+
and more specific error information for different failure scenarios.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ReveniumMiddlewareError(Exception):
|
|
10
|
+
"""Base exception for all Revenium middleware errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, original_error: Exception = None):
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.original_error = original_error
|
|
15
|
+
self.message = message
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
if self.original_error:
|
|
19
|
+
return f"{self.message} (caused by: {self.original_error})"
|
|
20
|
+
return self.message
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigurationError(ReveniumMiddlewareError):
|
|
24
|
+
"""Raised when there are configuration-related issues."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValidationError(ReveniumMiddlewareError):
|
|
29
|
+
"""Raised when input validation fails."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProviderDetectionError(ReveniumMiddlewareError):
|
|
34
|
+
"""Raised when provider detection fails."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MeteringError(ReveniumMiddlewareError):
|
|
39
|
+
"""Raised when metering operations fail."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NetworkError(ReveniumMiddlewareError):
|
|
44
|
+
"""Raised when network operations fail."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AuthenticationError(ReveniumMiddlewareError):
|
|
49
|
+
"""Raised when authentication fails."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StreamingError(ReveniumMiddlewareError):
|
|
54
|
+
"""Raised when streaming operations fail."""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ModelResolutionError(ReveniumMiddlewareError):
|
|
59
|
+
"""Raised when Azure model name resolution fails."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def handle_exception_safely(func):
|
|
64
|
+
"""
|
|
65
|
+
Decorator to handle exceptions safely without breaking the main application flow.
|
|
66
|
+
|
|
67
|
+
This decorator ensures that middleware errors never propagate to break
|
|
68
|
+
the user's application, following the principle of graceful degradation.
|
|
69
|
+
"""
|
|
70
|
+
def wrapper(*args, **kwargs):
|
|
71
|
+
try:
|
|
72
|
+
return func(*args, **kwargs)
|
|
73
|
+
except ReveniumMiddlewareError as e:
|
|
74
|
+
# Log middleware-specific errors
|
|
75
|
+
import logging
|
|
76
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
77
|
+
logger.error(f"Revenium middleware error in {func.__name__}: {e}")
|
|
78
|
+
return None
|
|
79
|
+
except Exception as e:
|
|
80
|
+
# Log unexpected errors
|
|
81
|
+
import logging
|
|
82
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
83
|
+
logger.error(f"Unexpected error in {func.__name__}: {e}", exc_info=True)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
return wrapper
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def categorize_exception(exception: Exception) -> ReveniumMiddlewareError:
|
|
90
|
+
"""
|
|
91
|
+
Categorize a generic exception into a more specific Revenium middleware exception.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
exception: The original exception to categorize
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A more specific ReveniumMiddlewareError subclass
|
|
98
|
+
"""
|
|
99
|
+
error_message = str(exception)
|
|
100
|
+
error_type = type(exception).__name__
|
|
101
|
+
|
|
102
|
+
# Network-related errors
|
|
103
|
+
if any(keyword in error_message.lower() for keyword in ['connection', 'timeout', 'network', 'dns']):
|
|
104
|
+
return NetworkError(f"Network error: {error_message}", exception)
|
|
105
|
+
|
|
106
|
+
# Authentication errors
|
|
107
|
+
if any(keyword in error_message.lower() for keyword in ['auth', 'unauthorized', 'forbidden', 'api key']):
|
|
108
|
+
return AuthenticationError(f"Authentication error: {error_message}", exception)
|
|
109
|
+
|
|
110
|
+
# Validation errors
|
|
111
|
+
if any(keyword in error_type.lower() for keyword in ['value', 'type', 'attribute']):
|
|
112
|
+
return ValidationError(f"Validation error: {error_message}", exception)
|
|
113
|
+
|
|
114
|
+
# Default to generic middleware error
|
|
115
|
+
return ReveniumMiddlewareError(f"Middleware error ({error_type}): {error_message}", exception)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain integration for Revenium middleware OpenAI.
|
|
3
|
+
|
|
4
|
+
This module provides zero-touch integration with LangChain applications through
|
|
5
|
+
callback handlers that automatically track usage and costs.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from revenium_middleware.openai.langchain import wrap
|
|
9
|
+
from langchain_openai import ChatOpenAI
|
|
10
|
+
|
|
11
|
+
llm = wrap(ChatOpenAI(model="gpt-4o-mini"))
|
|
12
|
+
response = llm.invoke("Hello Revenium!")
|
|
13
|
+
|
|
14
|
+
Installation:
|
|
15
|
+
pip install revenium-python-sdk[openai,langchain]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any, TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
# Only import types during type checking to avoid runtime dependencies
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
try:
|
|
23
|
+
# LangChain 1.0+ uses langchain_core
|
|
24
|
+
from langchain_core.language_models import BaseLanguageModel # noqa: F401
|
|
25
|
+
from langchain_core.embeddings import Embeddings # noqa: F401
|
|
26
|
+
except ImportError:
|
|
27
|
+
# LangChain 0.x uses langchain.schema and langchain.embeddings.base
|
|
28
|
+
from langchain.schema import BaseLanguageModel # noqa: F401
|
|
29
|
+
from langchain.embeddings.base import Embeddings # noqa: F401
|
|
30
|
+
|
|
31
|
+
# Import our enhanced dependency checking utilities
|
|
32
|
+
from ._utils import require_langchain_or_raise, is_langchain_available # noqa: F401
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Lazy loading implementation using __getattr__
|
|
36
|
+
def __getattr__(name: str) -> Any:
|
|
37
|
+
"""
|
|
38
|
+
Implement lazy loading for LangChain-dependent functionality.
|
|
39
|
+
|
|
40
|
+
This allows the module to be imported without LangChain installed,
|
|
41
|
+
but raises clear errors when trying to use LangChain-specific features.
|
|
42
|
+
"""
|
|
43
|
+
if name in ("wrap", "attach_to"):
|
|
44
|
+
require_langchain_or_raise(f"the {name}() function")
|
|
45
|
+
# Import the unified handler and create wrapper functions
|
|
46
|
+
from .unified_handler import UnifiedReveniumCallbackHandler # noqa: F401
|
|
47
|
+
|
|
48
|
+
def wrap(llm, **kwargs):
|
|
49
|
+
"""Wrap a LangChain LLM with Revenium usage tracking."""
|
|
50
|
+
handler = UnifiedReveniumCallbackHandler(**kwargs)
|
|
51
|
+
|
|
52
|
+
# Attach handler to LLM
|
|
53
|
+
if hasattr(llm, 'callbacks'):
|
|
54
|
+
if llm.callbacks is None:
|
|
55
|
+
llm.callbacks = [handler]
|
|
56
|
+
elif isinstance(llm.callbacks, list):
|
|
57
|
+
llm.callbacks.append(handler)
|
|
58
|
+
else:
|
|
59
|
+
# Convert to list if it's something else
|
|
60
|
+
llm.callbacks = [llm.callbacks, handler]
|
|
61
|
+
else:
|
|
62
|
+
# Try to add callbacks attribute
|
|
63
|
+
try:
|
|
64
|
+
llm.callbacks = [handler]
|
|
65
|
+
except Exception:
|
|
66
|
+
# Some models don't support callbacks
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
return llm
|
|
70
|
+
|
|
71
|
+
def attach_to(llm, **kwargs):
|
|
72
|
+
"""Attach Revenium usage tracking to an existing LangChain LLM in-place."""
|
|
73
|
+
return wrap(llm, **kwargs)
|
|
74
|
+
|
|
75
|
+
# Cache the functions in the module namespace
|
|
76
|
+
if name == "wrap":
|
|
77
|
+
globals()[name] = wrap
|
|
78
|
+
return wrap
|
|
79
|
+
else: # attach_to
|
|
80
|
+
globals()[name] = attach_to
|
|
81
|
+
return attach_to
|
|
82
|
+
|
|
83
|
+
elif name in ("ReveniumCallbackHandler", "UnifiedReveniumCallbackHandler"):
|
|
84
|
+
require_langchain_or_raise(f"the {name} class")
|
|
85
|
+
from .unified_handler import UnifiedReveniumCallbackHandler # noqa: F401
|
|
86
|
+
|
|
87
|
+
# Both names point to the same unified handler
|
|
88
|
+
globals()["ReveniumCallbackHandler"] = UnifiedReveniumCallbackHandler
|
|
89
|
+
globals()["UnifiedReveniumCallbackHandler"] = UnifiedReveniumCallbackHandler
|
|
90
|
+
|
|
91
|
+
return UnifiedReveniumCallbackHandler
|
|
92
|
+
|
|
93
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Convenience function for checking availability
|
|
97
|
+
def check_availability() -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Check if LangChain is available for use with this integration.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if LangChain is available, False otherwise
|
|
103
|
+
"""
|
|
104
|
+
return is_langchain_available()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Define __all__ for explicit exports
|
|
108
|
+
__all__ = [
|
|
109
|
+
"wrap",
|
|
110
|
+
"attach_to",
|
|
111
|
+
"ReveniumCallbackHandler",
|
|
112
|
+
"UnifiedReveniumCallbackHandler",
|
|
113
|
+
"check_availability",
|
|
114
|
+
]
|