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