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,184 @@
1
+ """
2
+ Revenium Middleware — the economic system of record for AI usage.
3
+
4
+ This package provides core metering functionality and provider-specific
5
+ middleware for deeply attributed AI usage metrics.
6
+ """
7
+ import os
8
+ import sys
9
+ import logging
10
+ import re
11
+
12
+
13
+ class ReadableFormatter(logging.Formatter):
14
+ """
15
+ Custom formatter that improves readability of log messages.
16
+ - Adds visual separators for important events
17
+ - Uses colors if terminal supports them
18
+ - Keeps messages clean and scannable
19
+ """
20
+
21
+ # ANSI color codes
22
+ COLORS = {
23
+ 'RESET': '\033[0m',
24
+ 'BOLD': '\033[1m',
25
+ 'DIM': '\033[2m',
26
+ 'GREEN': '\033[92m',
27
+ 'YELLOW': '\033[93m',
28
+ 'BLUE': '\033[94m',
29
+ 'CYAN': '\033[96m',
30
+ 'RED': '\033[91m',
31
+ }
32
+
33
+ # Box drawing characters
34
+ SEPARATOR = "─" * 70
35
+
36
+ def __init__(self, *args, **kwargs):
37
+ super().__init__(*args, **kwargs)
38
+ self.use_colors = self._supports_color()
39
+
40
+ def _supports_color(self):
41
+ """Check if terminal supports ANSI colors."""
42
+ return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
43
+
44
+ def _colorize(self, text, color):
45
+ """Apply color to text if terminal supports it."""
46
+ if self.use_colors and color in self.COLORS:
47
+ return f"{self.COLORS[color]}{text}{self.COLORS['RESET']}"
48
+ return text
49
+
50
+ def _truncate_long_objects(self, message, max_length=200):
51
+ """Truncate very long object representations to keep logs readable."""
52
+ # Check if message contains object representations
53
+ if '(' in message and ')' in message and len(message) > max_length:
54
+ # Look for patterns like "ClassName(field=value, ...)"
55
+ # Match object representations
56
+ pattern = r'(\w+)\(([^)]{100,})\)'
57
+
58
+ def replacer(match):
59
+ class_name = match.group(1)
60
+ content = match.group(2)
61
+ # Truncate the content
62
+ if len(content) > 150:
63
+ truncated = content[:150] + "..."
64
+ return f"{class_name}({truncated})"
65
+ return match.group(0)
66
+
67
+ message = re.sub(pattern, replacer, message)
68
+
69
+ return message
70
+
71
+ def format(self, record):
72
+ # Format the base message
73
+ message = record.getMessage()
74
+
75
+ # Truncate very long object representations in DEBUG logs
76
+ if record.levelname == 'DEBUG':
77
+ message = self._truncate_long_objects(message)
78
+
79
+ # Add visual enhancements for key events
80
+ # Check log level first to ensure it takes precedence
81
+ if record.levelname == 'ERROR':
82
+ message = self._colorize(f"[ERROR] {message}", 'RED')
83
+ elif record.levelname == 'WARNING':
84
+ message = self._colorize(f"[WARNING] {message}", 'YELLOW')
85
+ elif "SUCCESS" in message or "successful" in message.lower():
86
+ message = self._colorize(f"[SUCCESS] {message}", 'GREEN')
87
+ elif "FAILURE" in message:
88
+ message = self._colorize(f"[ERROR] {message}", 'RED')
89
+ elif "Shutdown complete" in message:
90
+ # Make shutdown completion more visible
91
+ separator = self._colorize(self.SEPARATOR, 'GREEN')
92
+ success_msg = self._colorize(
93
+ "[COMPLETE] All operations finished successfully", 'GREEN'
94
+ )
95
+ message = f"\n{separator}\n{success_msg}\n{separator}"
96
+ elif "Shutdown initiated" in message:
97
+ separator = self._colorize(self.SEPARATOR, 'CYAN')
98
+ shutdown_msg = self._colorize('[SHUTDOWN] ' + message, 'CYAN')
99
+ message = f"\n{separator}\n{shutdown_msg}"
100
+
101
+ # Format timestamp and level
102
+ if record.levelname == 'DEBUG':
103
+ level = self._colorize('DEBUG', 'DIM')
104
+ elif record.levelname == 'INFO':
105
+ level = self._colorize('INFO', 'BLUE')
106
+ elif record.levelname == 'WARNING':
107
+ level = self._colorize('WARN', 'YELLOW')
108
+ elif record.levelname == 'ERROR':
109
+ level = self._colorize('ERROR', 'RED')
110
+ else:
111
+ level = record.levelname
112
+
113
+ # Build the final log message
114
+ timestamp = self.formatTime(record, '%H:%M:%S')
115
+ return f"{timestamp} [{level}] {message}"
116
+
117
+
118
+ # Set up logger
119
+ logger = logging.getLogger("revenium_middleware")
120
+ log_level = os.environ.get("REVENIUM_LOG_LEVEL", "INFO").upper()
121
+ try:
122
+ logger.setLevel(getattr(logging, log_level))
123
+ except AttributeError:
124
+ logger.setLevel(logging.INFO)
125
+ logger.warning(f"Invalid log level: {log_level}, defaulting to INFO")
126
+
127
+ # Configure a handler with the readable formatter if none exists
128
+ if not logger.handlers and not logging.root.handlers:
129
+ handler = logging.StreamHandler()
130
+ formatter = ReadableFormatter()
131
+ handler.setFormatter(formatter)
132
+ logger.addHandler(handler)
133
+
134
+ # Allow propagation to root logger for testing
135
+ logger.propagate = True
136
+
137
+ # Re-export everything from _core for backward compatibility.
138
+ # Existing imports like `from revenium_middleware import client` keep working.
139
+ from ._core import ( # noqa: E402
140
+ client,
141
+ run_async_in_thread,
142
+ shutdown_event,
143
+ revenium_meter,
144
+ revenium_metadata,
145
+ track_usage,
146
+ is_inside_decorated_function,
147
+ get_function_metadata,
148
+ set_decorated_context,
149
+ clear_decorated_context,
150
+ get_injected_metadata,
151
+ set_injected_metadata,
152
+ clear_injected_metadata,
153
+ merge_metadata,
154
+ is_selective_metering_enabled,
155
+ )
156
+
157
+ # Re-export tool metering utilities from revenium_metering (v6.8.2+)
158
+ from revenium_metering import meter_tool, report_tool_call, configure # noqa: E402
159
+
160
+ __all__ = [
161
+ # Metering exports
162
+ "client",
163
+ "run_async_in_thread",
164
+ "shutdown_event",
165
+ # Decorator exports
166
+ "revenium_meter",
167
+ "revenium_metadata",
168
+ "track_usage",
169
+ # Context management exports
170
+ "is_inside_decorated_function",
171
+ "get_function_metadata",
172
+ "set_decorated_context",
173
+ "clear_decorated_context",
174
+ "get_injected_metadata",
175
+ "set_injected_metadata",
176
+ "clear_injected_metadata",
177
+ "merge_metadata",
178
+ # Config exports
179
+ "is_selective_metering_enabled",
180
+ # Tool metering exports (from revenium_metering)
181
+ "meter_tool",
182
+ "report_tool_call",
183
+ "configure",
184
+ ]
@@ -0,0 +1,65 @@
1
+ """
2
+ Core functionality for Revenium middleware.
3
+
4
+ This subpackage contains the foundational components shared across all
5
+ provider-specific middleware implementations.
6
+ """
7
+
8
+ from .metering import run_async_in_thread, shutdown_event, client
9
+ from .context import (
10
+ is_inside_decorated_function,
11
+ get_function_metadata,
12
+ set_decorated_context,
13
+ clear_decorated_context,
14
+ get_injected_metadata,
15
+ set_injected_metadata,
16
+ clear_injected_metadata,
17
+ merge_metadata,
18
+ )
19
+ from .decorators import revenium_meter, revenium_metadata, track_usage
20
+ from .config import is_selective_metering_enabled
21
+ from .trace_fields import (
22
+ get_environment,
23
+ get_region,
24
+ get_credential_alias,
25
+ get_trace_type,
26
+ get_trace_name,
27
+ get_parent_transaction_id,
28
+ get_transaction_name,
29
+ get_retry_number,
30
+ validate_trace_type,
31
+ validate_trace_name,
32
+ )
33
+
34
+ __all__ = [
35
+ # Metering
36
+ "client",
37
+ "run_async_in_thread",
38
+ "shutdown_event",
39
+ # Decorators
40
+ "revenium_meter",
41
+ "revenium_metadata",
42
+ "track_usage",
43
+ # Context management
44
+ "is_inside_decorated_function",
45
+ "get_function_metadata",
46
+ "set_decorated_context",
47
+ "clear_decorated_context",
48
+ "get_injected_metadata",
49
+ "set_injected_metadata",
50
+ "clear_injected_metadata",
51
+ "merge_metadata",
52
+ # Config
53
+ "is_selective_metering_enabled",
54
+ # Trace fields
55
+ "get_environment",
56
+ "get_region",
57
+ "get_credential_alias",
58
+ "get_trace_type",
59
+ "get_trace_name",
60
+ "get_parent_transaction_id",
61
+ "get_transaction_name",
62
+ "get_retry_number",
63
+ "validate_trace_type",
64
+ "validate_trace_name",
65
+ ]
@@ -0,0 +1,165 @@
1
+ """
2
+ Shared configuration constants and settings for Revenium middleware.
3
+
4
+ This module is the single source of truth for configuration values shared
5
+ across all provider middlewares. Provider-specific config files extend
6
+ this via class inheritance and re-export symbols for backward compatibility.
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ from typing import Set, Literal, Union, Optional
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Type alias for summary format
16
+ SummaryFormat = Literal["human", "json"]
17
+
18
+
19
+ class Config:
20
+ """Shared configuration constants for all Revenium middleware providers."""
21
+
22
+ # Threading and async timeouts
23
+ THREAD_JOIN_TIMEOUT: float = 5.0
24
+ API_REQUEST_TIMEOUT: float = 30.0
25
+ BACKGROUND_THREAD_TIMEOUT: float = 5.0
26
+
27
+ # Cache settings
28
+ PROVIDER_CACHE_TTL: int = 3600 # 1 hour
29
+ MODEL_CACHE_TTL: int = 3600 # 1 hour
30
+
31
+ # Logging and security
32
+ MAX_LOG_STRING_LENGTH: int = 100
33
+ MAX_SANITIZATION_DEPTH: int = 3
34
+
35
+ # Stream processing
36
+ STREAM_CHUNK_BUFFER_SIZE: int = 1000
37
+ STREAM_TIMEOUT: float = 30.0
38
+
39
+ # Retry and resilience
40
+ MAX_RETRY_ATTEMPTS: int = 3
41
+ RETRY_BACKOFF_FACTOR: float = 1.5
42
+ INITIAL_RETRY_DELAY: float = 0.1
43
+
44
+ # Core environment variable names
45
+ ENV_REVENIUM_API_KEY: str = "REVENIUM_METERING_API_KEY"
46
+ ENV_LOG_LEVEL: str = "REVENIUM_LOG_LEVEL"
47
+
48
+ # Trace visualization environment variables
49
+ ENV_REVENIUM_ENVIRONMENT: str = "REVENIUM_ENVIRONMENT"
50
+ ENV_ENVIRONMENT: str = "ENVIRONMENT"
51
+ ENV_DEPLOYMENT_ENV: str = "DEPLOYMENT_ENV"
52
+ ENV_REVENIUM_REGION: str = "REVENIUM_REGION"
53
+ ENV_REVENIUM_CREDENTIAL_ALIAS: str = "REVENIUM_CREDENTIAL_ALIAS"
54
+ ENV_REVENIUM_TRACE_TYPE: str = "REVENIUM_TRACE_TYPE"
55
+ ENV_REVENIUM_TRACE_NAME: str = "REVENIUM_TRACE_NAME"
56
+ ENV_REVENIUM_PARENT_TRANSACTION_ID: str = "REVENIUM_PARENT_TRANSACTION_ID"
57
+ ENV_REVENIUM_TRANSACTION_NAME: str = "REVENIUM_TRANSACTION_NAME"
58
+ ENV_REVENIUM_RETRY_NUMBER: str = "REVENIUM_RETRY_NUMBER"
59
+
60
+ # Prompt capture settings
61
+ ENV_REVENIUM_CAPTURE_PROMPTS: str = "REVENIUM_CAPTURE_PROMPTS"
62
+ CAPTURE_PROMPTS: bool = os.getenv("REVENIUM_CAPTURE_PROMPTS", "false").lower() in ("true", "1", "yes", "on")
63
+ MAX_PROMPT_LENGTH: int = 50_000 # Maximum characters per prompt field
64
+
65
+ # Terminal summary settings
66
+ ENV_REVENIUM_PRINT_SUMMARY: str = "REVENIUM_PRINT_SUMMARY"
67
+ ENV_REVENIUM_TEAM_ID: str = "REVENIUM_TEAM_ID"
68
+ ENV_REVENIUM_BASE_URL: str = "REVENIUM_METERING_BASE_URL"
69
+ SUMMARY_RETRY_ATTEMPTS: int = 3
70
+ SUMMARY_RETRY_DELAY: float = 2.0
71
+ SUMMARY_API_TIMEOUT: float = 5.0
72
+ DEFAULT_BASE_URL: str = "https://api.revenium.ai"
73
+
74
+
75
+ class SecurityConfig:
76
+ """Security-related configuration shared across all providers."""
77
+
78
+ SENSITIVE_FIELDS: Set[str] = {
79
+ 'api_key', 'subscriber_credential', 'subscriber_email', 'messages',
80
+ 'input', 'content', 'prompt', 'text', 'data', 'authorization',
81
+ 'x-api-key', 'bearer', 'token', 'password', 'secret', 'key',
82
+ 'credential', 'auth', 'private',
83
+ 'system_prompt', 'input_messages', 'output_response'
84
+ }
85
+
86
+ SENSITIVE_PATTERNS: Set[str] = {
87
+ 'sk-', 'pk-', 'Bearer ', 'Basic ', 'Token '
88
+ }
89
+
90
+
91
+ def is_selective_metering_enabled() -> bool:
92
+ """
93
+ Check if selective metering is enabled.
94
+
95
+ When enabled, only functions decorated with @revenium_meter will be metered.
96
+ When disabled (default), all API calls are metered automatically.
97
+
98
+ The setting is controlled by the REVENIUM_SELECTIVE_METERING environment variable.
99
+ Accepted values for enabled: "true", "1", "yes", "on" (case-insensitive)
100
+ """
101
+ env_value = os.environ.get("REVENIUM_SELECTIVE_METERING", "false").lower()
102
+ return env_value in ("true", "1", "yes", "on")
103
+
104
+
105
+ def get_config_value(key: str, default: any = None) -> any:
106
+ """Get configuration value from environment or use default."""
107
+ return os.getenv(key, default)
108
+
109
+
110
+ def is_debug_enabled() -> bool:
111
+ """Check if debug logging is enabled."""
112
+ log_level = get_config_value(Config.ENV_LOG_LEVEL, "INFO").upper()
113
+ return log_level == "DEBUG"
114
+
115
+
116
+ def get_timeout_config() -> dict:
117
+ """Get base timeout-related configuration."""
118
+ return {
119
+ 'thread_join': Config.THREAD_JOIN_TIMEOUT,
120
+ 'api_request': Config.API_REQUEST_TIMEOUT,
121
+ 'background_thread': Config.BACKGROUND_THREAD_TIMEOUT,
122
+ 'stream': Config.STREAM_TIMEOUT,
123
+ }
124
+
125
+
126
+ def parse_print_summary_value(value: Optional[str]) -> Union[bool, SummaryFormat]:
127
+ """
128
+ Parse REVENIUM_PRINT_SUMMARY environment variable value.
129
+
130
+ Returns:
131
+ False if disabled, 'human' or 'json' if enabled
132
+ """
133
+ if value is None:
134
+ return False
135
+
136
+ value_lower = value.lower().strip()
137
+
138
+ if value_lower in ('false', '0', 'no', 'off', 'disabled', ''):
139
+ return False
140
+ elif value_lower in ('true', '1', 'yes', 'on', 'enabled', 'human'):
141
+ return 'human'
142
+ elif value_lower == 'json':
143
+ return 'json'
144
+ else:
145
+ logger.warning(
146
+ f"Invalid REVENIUM_PRINT_SUMMARY value '{value}'. "
147
+ f"Expected 'true', 'human', 'json', or 'false'. Defaulting to disabled."
148
+ )
149
+ return False
150
+
151
+
152
+ def get_print_summary_config() -> Union[bool, SummaryFormat]:
153
+ """Get print summary configuration from environment."""
154
+ value = get_config_value(Config.ENV_REVENIUM_PRINT_SUMMARY)
155
+ return parse_print_summary_value(value)
156
+
157
+
158
+ def get_team_id() -> Optional[str]:
159
+ """Get Revenium team ID from environment."""
160
+ return get_config_value(Config.ENV_REVENIUM_TEAM_ID)
161
+
162
+
163
+ def get_base_url() -> str:
164
+ """Get Revenium base URL from environment (defaults to https://api.revenium.ai)."""
165
+ return get_config_value(Config.ENV_REVENIUM_BASE_URL, Config.DEFAULT_BASE_URL)
@@ -0,0 +1,109 @@
1
+ """
2
+ Context tracking for selective metering and metadata injection with decorators.
3
+
4
+ This module provides thread-safe and async-safe context tracking to determine
5
+ whether code is currently executing inside a decorated function that should be metered,
6
+ and to store metadata that should be injected into API calls.
7
+ """
8
+
9
+ import contextvars
10
+ from typing import Optional, Dict, Any
11
+
12
+ # Context variable to track if we're inside a decorated function
13
+ _decorated_function_context: contextvars.ContextVar[bool] = contextvars.ContextVar(
14
+ 'revenium_decorated_function', default=False
15
+ )
16
+
17
+ # Context variable to store metadata from the current decorated function
18
+ _function_metadata_context: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar(
19
+ 'revenium_function_metadata', default=None
20
+ )
21
+
22
+ # Context variable to store injected metadata from @revenium_metadata decorator
23
+ _injected_metadata_context: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar(
24
+ 'revenium_injected_metadata', default=None
25
+ )
26
+
27
+
28
+ def is_inside_decorated_function() -> bool:
29
+ """
30
+ Check if code is currently executing inside a decorated function.
31
+
32
+ Returns:
33
+ True if inside a decorated function, False otherwise
34
+ """
35
+ return _decorated_function_context.get()
36
+
37
+
38
+ def get_function_metadata() -> Optional[Dict[str, Any]]:
39
+ """
40
+ Get metadata from the current decorated function context.
41
+
42
+ Returns:
43
+ Dictionary of metadata or None if not in decorated function
44
+ """
45
+ return _function_metadata_context.get()
46
+
47
+
48
+ def set_decorated_context(is_decorated: bool, metadata: Optional[Dict[str, Any]] = None) -> None:
49
+ """
50
+ Set the decorated function context.
51
+
52
+ Args:
53
+ is_decorated: Whether we're inside a decorated function
54
+ metadata: Optional metadata from the decorator
55
+ """
56
+ _decorated_function_context.set(is_decorated)
57
+ _function_metadata_context.set(metadata)
58
+
59
+
60
+ def clear_decorated_context() -> None:
61
+ """Clear the decorated function context."""
62
+ _decorated_function_context.set(False)
63
+ _function_metadata_context.set(None)
64
+
65
+
66
+ def get_injected_metadata() -> Optional[Dict[str, Any]]:
67
+ """
68
+ Get metadata from the current @revenium_metadata decorator context.
69
+
70
+ Returns:
71
+ Dictionary of injected metadata or None if not in decorated function
72
+ """
73
+ return _injected_metadata_context.get()
74
+
75
+
76
+ def set_injected_metadata(metadata: Optional[Dict[str, Any]]) -> None:
77
+ """
78
+ Set the injected metadata context.
79
+
80
+ Args:
81
+ metadata: Dictionary of metadata to inject into API calls
82
+ """
83
+ _injected_metadata_context.set(metadata)
84
+
85
+
86
+ def clear_injected_metadata() -> None:
87
+ """Clear the injected metadata context."""
88
+ _injected_metadata_context.set(None)
89
+
90
+
91
+ def merge_metadata(api_metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
92
+ """
93
+ Merge injected metadata with API-level metadata.
94
+
95
+ API-level metadata takes precedence over injected metadata.
96
+
97
+ Args:
98
+ api_metadata: Metadata passed directly to the API call
99
+
100
+ Returns:
101
+ Merged metadata dictionary with API-level metadata taking precedence
102
+ """
103
+ injected = get_injected_metadata() or {}
104
+ api = api_metadata or {}
105
+
106
+ # Start with injected metadata, then override with API-level metadata
107
+ merged = {**injected, **api}
108
+ return merged
109
+