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,158 @@
1
+ """
2
+ Trace visualization field capture and validation.
3
+
4
+ This module provides functions to capture trace visualization fields from
5
+ environment variables and validate them according to the specification.
6
+
7
+ Shared functions are imported from _core.trace_fields. This module retains
8
+ only Anthropic-specific functions: detect_vision_content and detect_operation_type.
9
+ """
10
+
11
+ from typing import Optional, Dict, Any
12
+
13
+ from revenium_middleware._core.trace_fields import ( # noqa: F401 — re-exported
14
+ TRACE_TYPE_MAX_LENGTH,
15
+ TRACE_NAME_MAX_LENGTH,
16
+ TRACE_TYPE_PATTERN,
17
+ get_environment,
18
+ get_region,
19
+ get_credential_alias,
20
+ get_trace_type,
21
+ get_trace_name,
22
+ get_parent_transaction_id,
23
+ get_transaction_name,
24
+ get_retry_number,
25
+ validate_trace_type,
26
+ validate_trace_name,
27
+ )
28
+
29
+
30
+ def detect_vision_content(messages: Optional[list] = None) -> bool:
31
+ """
32
+ Detect if messages contain vision/image content.
33
+
34
+ Anthropic vision content format:
35
+ {
36
+ "type": "image",
37
+ "source": {
38
+ "type": "base64" | "url",
39
+ "media_type": "image/jpeg",
40
+ "data": "..." # or "url": "..."
41
+ }
42
+ }
43
+
44
+ Args:
45
+ messages: List of message objects from Anthropic API request
46
+
47
+ Returns:
48
+ True if any message contains image content, False otherwise
49
+ """
50
+ if not messages:
51
+ return False
52
+
53
+ for message in messages:
54
+ if not isinstance(message, dict):
55
+ continue
56
+
57
+ content = message.get("content")
58
+ if content is None:
59
+ continue
60
+
61
+ # Content can be a string (no images) or a list of content blocks
62
+ if isinstance(content, list):
63
+ for block in content:
64
+ if isinstance(block, dict) and block.get("type") == "image":
65
+ return True
66
+
67
+ return False
68
+
69
+
70
+ def detect_operation_type(
71
+ provider: str,
72
+ endpoint: str,
73
+ request_body: Optional[Dict[str, Any]] = None
74
+ ) -> Dict[str, Optional[str]]:
75
+ """
76
+ Auto-detect operation type and subtype from provider, endpoint,
77
+ and request.
78
+
79
+ Args:
80
+ provider: Provider name (e.g., 'openai', 'azure_openai') or Provider enum
81
+ endpoint: API endpoint (e.g., '/chat/completions', '/embeddings')
82
+ request_body: Optional request body to check for tools/functions
83
+
84
+ Returns:
85
+ Dictionary with 'operationType' and 'operationSubtype' keys
86
+ """
87
+ # Handle Provider enum or string
88
+ if hasattr(provider, 'name'):
89
+ # It's a Provider enum, get the name (e.g., 'OPENAI', 'AZURE_OPENAI')
90
+ provider_str = provider.name
91
+ else:
92
+ provider_str = str(provider)
93
+
94
+ provider_lower = provider_str.lower()
95
+ request_body = request_body or {}
96
+
97
+ # OpenAI and Azure OpenAI
98
+ if provider_lower in ('openai', 'azure_openai', 'azure'):
99
+ # Chat completions
100
+ is_chat = (
101
+ 'chat/completions' in endpoint or
102
+ endpoint.endswith('/chat/completions')
103
+ )
104
+ if is_chat:
105
+ # Check for tools or functions
106
+ has_tools = (
107
+ request_body.get('tools') or
108
+ request_body.get('functions')
109
+ )
110
+ if has_tools:
111
+ return {
112
+ 'operationType': 'TOOL_CALL',
113
+ 'operationSubtype': 'function_call'
114
+ }
115
+ return {
116
+ 'operationType': 'CHAT',
117
+ 'operationSubtype': None
118
+ }
119
+
120
+ # Embeddings
121
+ if 'embeddings' in endpoint or endpoint.endswith('/embeddings'):
122
+ return {
123
+ 'operationType': 'EMBED',
124
+ 'operationSubtype': None
125
+ }
126
+
127
+ # Moderations
128
+ if 'moderations' in endpoint or endpoint.endswith('/moderations'):
129
+ return {
130
+ 'operationType': 'MODERATION',
131
+ 'operationSubtype': None
132
+ }
133
+
134
+ # Anthropic
135
+ if provider_lower in ('anthropic', 'bedrock'):
136
+ # Messages endpoint (chat completions)
137
+ is_messages = (
138
+ '/messages' in endpoint or
139
+ endpoint.endswith('/messages')
140
+ )
141
+ if is_messages:
142
+ # Check for tools
143
+ has_tools = request_body.get('tools')
144
+ if has_tools:
145
+ return {
146
+ 'operationType': 'TOOL_CALL',
147
+ 'operationSubtype': None
148
+ }
149
+ return {
150
+ 'operationType': 'CHAT',
151
+ 'operationSubtype': None
152
+ }
153
+
154
+ # Default fallback
155
+ return {
156
+ 'operationType': 'CHAT',
157
+ 'operationSubtype': None
158
+ }
@@ -0,0 +1,114 @@
1
+ """
2
+ Revenium middleware for Google AI services.
3
+
4
+ This package provides middleware to track and meter usage of Google AI services
5
+ including Google AI SDK (google-genai) and Vertex AI SDK (vertexai).
6
+
7
+ The middleware automatically wraps API calls to capture usage metrics and send
8
+ them to Revenium for tracking and billing purposes.
9
+
10
+ Usage:
11
+ Simply import this package before using Google AI services:
12
+
13
+ import revenium_middleware.google
14
+ from google import genai # or import vertexai
15
+
16
+ # Your existing code works unchanged
17
+ client = genai.Client(api_key="your-key")
18
+ response = client.models.generate_content(...)
19
+ """
20
+
21
+ import logging
22
+ import os
23
+
24
+
25
+ # Configure logging based on REVENIUM_LOG_LEVEL environment variable
26
+ def _configure_logging():
27
+ """Configure logging for the Revenium middleware."""
28
+ log_level_str = os.getenv("REVENIUM_LOG_LEVEL", "INFO").upper()
29
+
30
+ # Map string levels to logging constants
31
+ level_mapping = {
32
+ "DEBUG": logging.DEBUG,
33
+ "INFO": logging.INFO,
34
+ "WARNING": logging.WARNING,
35
+ "ERROR": logging.ERROR,
36
+ "CRITICAL": logging.CRITICAL,
37
+ }
38
+
39
+ log_level = level_mapping.get(log_level_str, logging.INFO)
40
+
41
+ # Configure the revenium middleware logger
42
+ revenium_logger = logging.getLogger("revenium_middleware")
43
+ revenium_logger.setLevel(log_level)
44
+
45
+ # Only add handler if none exists to avoid duplicate logs
46
+ if not revenium_logger.handlers:
47
+ handler = logging.StreamHandler()
48
+ formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
49
+ handler.setFormatter(formatter)
50
+ revenium_logger.addHandler(handler)
51
+
52
+ return log_level
53
+
54
+
55
+ # Configure logging
56
+ _log_level = _configure_logging()
57
+
58
+ # Set up logging
59
+ logger = logging.getLogger(__name__)
60
+
61
+ # Check if verbose startup logging is enabled
62
+ _verbose_startup = os.getenv("REVENIUM_VERBOSE_STARTUP", "").lower() in (
63
+ "true",
64
+ "1",
65
+ "yes",
66
+ )
67
+
68
+ if _verbose_startup:
69
+ logger.info("Revenium middleware initialization starting")
70
+
71
+ # Import common utilities (always available)
72
+ from .common import utils
73
+
74
+ # Import and activate middleware for available SDKs
75
+ try:
76
+ # Try to import Google AI SDK and activate middleware
77
+ if _verbose_startup:
78
+ logger.debug("Attempting to import google.genai")
79
+ import google.genai
80
+
81
+ if _verbose_startup:
82
+ logger.debug("google.genai imported successfully, importing middleware")
83
+ from .google_ai import middleware as google_ai_middleware
84
+
85
+ logger.info("Google AI SDK middleware activated")
86
+ except ImportError as e:
87
+ logger.debug("Google AI SDK (google-genai) not available: %s", e)
88
+
89
+ try:
90
+ # Try to import Vertex AI SDK and activate middleware
91
+ if _verbose_startup:
92
+ logger.debug("Attempting to import vertexai")
93
+ import vertexai
94
+
95
+ if _verbose_startup:
96
+ logger.debug("vertexai imported successfully, importing middleware")
97
+ from .vertex_ai import middleware as vertex_ai_middleware
98
+
99
+ logger.info("Vertex AI SDK middleware activated")
100
+ except ImportError as e:
101
+ logger.debug("Vertex AI SDK (vertexai) not available: %s", e)
102
+
103
+ active_sdks = []
104
+ if "google.genai" in globals():
105
+ active_sdks.append("google_ai")
106
+ if "vertexai" in globals():
107
+ active_sdks.append("vertex_ai")
108
+
109
+ logger.info(
110
+ "Revenium middleware activated for: %s",
111
+ ", ".join(active_sdks) if active_sdks else "none",
112
+ )
113
+
114
+ __all__ = ["utils"]
@@ -0,0 +1,127 @@
1
+ """
2
+ Common utilities and shared types for Google AI middleware.
3
+
4
+ This module contains shared functionality used by both Google AI SDK
5
+ and Vertex AI SDK middleware implementations.
6
+ """
7
+
8
+ from .types import (
9
+ OperationType,
10
+ Provider,
11
+ ProviderMetadata,
12
+ UsageData,
13
+ TokenCounts,
14
+ normalize_stop_reason,
15
+ GOOGLE_AI_STOP_REASONS,
16
+ VERTEX_AI_STOP_REASONS,
17
+ )
18
+
19
+ from .utils import (
20
+ generate_transaction_id,
21
+ format_timestamp,
22
+ calculate_duration_ms,
23
+ log_token_usage,
24
+ log_image_usage,
25
+ log_video_usage,
26
+ create_metering_call,
27
+ create_image_metering_call,
28
+ create_video_metering_call,
29
+ extract_model_name,
30
+ extract_token_counts,
31
+ create_usage_data,
32
+ is_debug_logging_enabled,
33
+ )
34
+
35
+ from .exceptions import (
36
+ ReveniumMiddlewareError,
37
+ MeteringError,
38
+ TokenExtractionError,
39
+ ProviderDetectionError,
40
+ ConfigurationError,
41
+ StreamingError,
42
+ APIResponseError,
43
+ handle_metering_error,
44
+ safe_extract,
45
+ )
46
+
47
+ from .protocols import (
48
+ UsageMetadataProtocol,
49
+ CandidateProtocol,
50
+ EmbeddingProtocol,
51
+ ChatResponseProtocol,
52
+ EmbeddingResponseProtocol,
53
+ StreamChunkProtocol,
54
+ ClientProtocol,
55
+ is_chat_response,
56
+ is_embedding_response,
57
+ is_stream_chunk,
58
+ has_usage_metadata,
59
+ has_token_counts,
60
+ safe_getattr,
61
+ get_token_count,
62
+ )
63
+
64
+ from .summary_printer import (
65
+ CompletionMetrics,
66
+ fetch_completion_metrics,
67
+ format_and_print_json_summary,
68
+ format_and_print_human_summary,
69
+ print_usage_summary,
70
+ )
71
+
72
+ __all__ = [
73
+ # Types
74
+ "OperationType",
75
+ "Provider",
76
+ "ProviderMetadata",
77
+ "UsageData",
78
+ "TokenCounts",
79
+ "normalize_stop_reason",
80
+ "GOOGLE_AI_STOP_REASONS",
81
+ "VERTEX_AI_STOP_REASONS",
82
+ # Utils
83
+ "generate_transaction_id",
84
+ "format_timestamp",
85
+ "calculate_duration_ms",
86
+ "log_token_usage",
87
+ "log_image_usage",
88
+ "log_video_usage",
89
+ "create_metering_call",
90
+ "create_image_metering_call",
91
+ "create_video_metering_call",
92
+ "extract_model_name",
93
+ "extract_token_counts",
94
+ "create_usage_data",
95
+ "is_debug_logging_enabled",
96
+ # Exceptions
97
+ "ReveniumMiddlewareError",
98
+ "MeteringError",
99
+ "TokenExtractionError",
100
+ "ProviderDetectionError",
101
+ "ConfigurationError",
102
+ "StreamingError",
103
+ "APIResponseError",
104
+ "handle_metering_error",
105
+ "safe_extract",
106
+ # Protocols
107
+ "UsageMetadataProtocol",
108
+ "CandidateProtocol",
109
+ "EmbeddingProtocol",
110
+ "ChatResponseProtocol",
111
+ "EmbeddingResponseProtocol",
112
+ "StreamChunkProtocol",
113
+ "ClientProtocol",
114
+ "is_chat_response",
115
+ "is_embedding_response",
116
+ "is_stream_chunk",
117
+ "has_usage_metadata",
118
+ "has_token_counts",
119
+ "safe_getattr",
120
+ "get_token_count",
121
+ # Summary Printer
122
+ "CompletionMetrics",
123
+ "fetch_completion_metrics",
124
+ "format_and_print_json_summary",
125
+ "format_and_print_human_summary",
126
+ "print_usage_summary",
127
+ ]
@@ -0,0 +1,137 @@
1
+ """
2
+ Custom exception classes for Revenium Google AI middleware.
3
+
4
+ This module defines a hierarchy of exceptions for different error scenarios
5
+ that can occur during middleware operation.
6
+ """
7
+
8
+ from typing import Optional, Any
9
+
10
+
11
+ class ReveniumMiddlewareError(Exception):
12
+ """Base exception for all Revenium middleware errors."""
13
+
14
+ def __init__(self, message: str, details: Optional[dict] = None):
15
+ super().__init__(message)
16
+ self.message = message
17
+ self.details = details or {}
18
+
19
+ def __str__(self) -> str:
20
+ if self.details:
21
+ return f"{self.message} (Details: {self.details})"
22
+ return self.message
23
+
24
+
25
+ class MeteringError(ReveniumMiddlewareError):
26
+ """Raised when metering operations fail."""
27
+
28
+ def __init__(
29
+ self,
30
+ message: str,
31
+ transaction_id: Optional[str] = None,
32
+ api_response: Optional[Any] = None,
33
+ **kwargs,
34
+ ):
35
+ super().__init__(message, kwargs)
36
+ self.transaction_id = transaction_id
37
+ self.api_response = api_response
38
+
39
+
40
+ class TokenExtractionError(ReveniumMiddlewareError):
41
+ """Raised when token count extraction fails."""
42
+
43
+ def __init__(
44
+ self,
45
+ message: str,
46
+ response_type: Optional[str] = None,
47
+ operation_type: Optional[str] = None,
48
+ **kwargs,
49
+ ):
50
+ super().__init__(message, kwargs)
51
+ self.response_type = response_type
52
+ self.operation_type = operation_type
53
+
54
+
55
+ class ProviderDetectionError(ReveniumMiddlewareError):
56
+ """Raised when provider detection fails."""
57
+
58
+ def __init__(self, message: str, available_sdks: Optional[list] = None, **kwargs):
59
+ super().__init__(message, kwargs)
60
+ self.available_sdks = available_sdks or []
61
+
62
+
63
+ class ConfigurationError(ReveniumMiddlewareError):
64
+ """Raised when configuration is invalid or missing."""
65
+
66
+ def __init__(self, message: str, missing_config: Optional[list] = None, **kwargs):
67
+ super().__init__(message, kwargs)
68
+ self.missing_config = missing_config or []
69
+
70
+
71
+ class StreamingError(ReveniumMiddlewareError):
72
+ """Raised when streaming operations fail."""
73
+
74
+ def __init__(
75
+ self,
76
+ message: str,
77
+ chunk_count: Optional[int] = None,
78
+ stream_state: Optional[str] = None,
79
+ **kwargs,
80
+ ):
81
+ super().__init__(message, kwargs)
82
+ self.chunk_count = chunk_count
83
+ self.stream_state = stream_state
84
+
85
+
86
+ class APIResponseError(ReveniumMiddlewareError):
87
+ """Raised when API response is malformed or unexpected."""
88
+
89
+ def __init__(
90
+ self,
91
+ message: str,
92
+ response_data: Optional[Any] = None,
93
+ expected_format: Optional[str] = None,
94
+ **kwargs,
95
+ ):
96
+ super().__init__(message, kwargs)
97
+ self.response_data = response_data
98
+ self.expected_format = expected_format
99
+
100
+
101
+ # Utility functions for error handling
102
+ def handle_metering_error(func):
103
+ """Decorator to handle metering errors gracefully."""
104
+
105
+ def wrapper(*args, **kwargs):
106
+ try:
107
+ return func(*args, **kwargs)
108
+ except MeteringError:
109
+ # Re-raise metering errors as-is
110
+ raise
111
+ except Exception as e:
112
+ # Convert other exceptions to MeteringError
113
+ raise MeteringError(
114
+ f"Unexpected error in {func.__name__}: {str(e)}",
115
+ details={"original_error": type(e).__name__},
116
+ ) from e
117
+
118
+ return wrapper
119
+
120
+
121
+ def safe_extract(func):
122
+ """Decorator to handle extraction errors gracefully."""
123
+
124
+ def wrapper(*args, **kwargs):
125
+ try:
126
+ return func(*args, **kwargs)
127
+ except (TokenExtractionError, APIResponseError):
128
+ # Re-raise extraction errors as-is
129
+ raise
130
+ except Exception as e:
131
+ # Convert other exceptions to TokenExtractionError
132
+ raise TokenExtractionError(
133
+ f"Unexpected error in {func.__name__}: {str(e)}",
134
+ details={"original_error": type(e).__name__},
135
+ ) from e
136
+
137
+ return wrapper