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,178 @@
1
+ """
2
+ Prompt extraction utilities for capturing AI prompts and responses.
3
+
4
+ This module provides functions to extract and truncate prompts from Anthropic API
5
+ requests and responses for optional storage in Revenium analytics.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from typing import Dict, Any, Optional, List
11
+
12
+ from .config import Config
13
+ from revenium_middleware._core.prompt_extraction import ( # noqa: F401 — re-exported
14
+ extract_streaming_response_content,
15
+ )
16
+
17
+ logger = logging.getLogger("revenium_middleware.extension")
18
+
19
+
20
+ def extract_prompts_from_request(kwargs: Dict[str, Any]) -> Dict[str, Any]:
21
+ """
22
+ Extract system prompt and input messages from Anthropic API request.
23
+
24
+ Args:
25
+ kwargs: The kwargs dict passed to the Anthropic API call
26
+
27
+ Returns:
28
+ Dict containing:
29
+ - systemPrompt: String or None (system message content)
30
+ - inputMessages: JSON string or None (user/assistant messages)
31
+ - promptsTruncated: Boolean (True if any field was truncated)
32
+ """
33
+ # Extract system prompt (Anthropic has separate 'system' parameter)
34
+ system_prompt = kwargs.get('system')
35
+ messages = kwargs.get('messages', [])
36
+ prompts_truncated = False
37
+
38
+ # Apply truncation to system prompt
39
+ if system_prompt:
40
+ if isinstance(system_prompt, list):
41
+ # System can be an array of text blocks
42
+ try:
43
+ system_prompt = json.dumps(system_prompt, ensure_ascii=False)
44
+ except (TypeError, ValueError) as e:
45
+ logger.warning(f"Failed to serialize system prompt to JSON: {e}")
46
+ system_prompt = str(system_prompt)
47
+
48
+ if len(system_prompt) > Config.MAX_PROMPT_LENGTH:
49
+ marker = "...[TRUNCATED]"
50
+ marker_len = len(marker)
51
+ truncate_at = Config.MAX_PROMPT_LENGTH - marker_len
52
+ system_prompt = system_prompt[:truncate_at] + marker
53
+ prompts_truncated = True
54
+ logger.debug(f"System prompt truncated to {Config.MAX_PROMPT_LENGTH} characters")
55
+
56
+ # Convert messages to JSON string
57
+ input_messages = None
58
+ if messages:
59
+ try:
60
+ # First, truncate individual message contents to avoid invalid JSON
61
+ marker = "...[TRUNCATED]"
62
+ marker_len = len(marker)
63
+ truncated_messages = []
64
+
65
+ for msg in messages:
66
+ if not isinstance(msg, dict):
67
+ continue
68
+
69
+ truncated_msg = msg.copy()
70
+ content = msg.get('content', '')
71
+
72
+ # Handle content as string
73
+ if isinstance(content, str) and len(content) > Config.MAX_PROMPT_LENGTH // 2:
74
+ truncate_at = (Config.MAX_PROMPT_LENGTH // 2) - marker_len
75
+ truncated_msg['content'] = content[:truncate_at] + marker
76
+ prompts_truncated = True
77
+ # Handle content as array (multimodal - text, images, etc.)
78
+ elif isinstance(content, list):
79
+ truncated_content = []
80
+ for block in content:
81
+ if isinstance(block, dict):
82
+ truncated_block = block.copy()
83
+ # Truncate text blocks
84
+ if block.get('type') == 'text':
85
+ text = block.get('text', '')
86
+ if len(text) > Config.MAX_PROMPT_LENGTH // 2:
87
+ truncate_at = (Config.MAX_PROMPT_LENGTH // 2) - marker_len
88
+ truncated_block['text'] = text[:truncate_at] + marker
89
+ prompts_truncated = True
90
+ truncated_content.append(truncated_block)
91
+ else:
92
+ truncated_content.append(block)
93
+ truncated_msg['content'] = truncated_content
94
+
95
+ truncated_messages.append(truncated_msg)
96
+
97
+ input_messages = json.dumps(truncated_messages, ensure_ascii=False)
98
+
99
+ # Apply final truncation if still too long
100
+ if len(input_messages) > Config.MAX_PROMPT_LENGTH:
101
+ truncate_at = Config.MAX_PROMPT_LENGTH - marker_len
102
+ input_messages = input_messages[:truncate_at] + marker
103
+ prompts_truncated = True
104
+ logger.debug(
105
+ f"Input messages truncated to "
106
+ f"{Config.MAX_PROMPT_LENGTH} characters"
107
+ )
108
+ except (TypeError, ValueError) as e:
109
+ logger.warning(f"Failed to serialize input messages to JSON: {e}")
110
+ input_messages = None
111
+
112
+ return {
113
+ 'systemPrompt': system_prompt,
114
+ 'inputMessages': input_messages,
115
+ 'promptsTruncated': prompts_truncated
116
+ }
117
+
118
+
119
+ def extract_response_content(response: Any, prompts_truncated: bool = False) -> Dict[str, Any]:
120
+ """
121
+ Extract output response content from Anthropic API response.
122
+
123
+ Args:
124
+ response: Anthropic API response object (Message)
125
+ prompts_truncated: Whether prompts were already truncated (from request)
126
+
127
+ Returns:
128
+ Dict containing:
129
+ - outputResponse: String or None (assistant response content)
130
+ - promptsTruncated: Boolean (True if any field was truncated)
131
+ """
132
+ output_response = None
133
+ was_truncated = prompts_truncated
134
+
135
+ try:
136
+ # Extract content from response.content (array of content blocks)
137
+ if hasattr(response, 'content') and response.content:
138
+ content_blocks = []
139
+
140
+ for block in response.content:
141
+ if hasattr(block, 'type'):
142
+ if block.type == 'text' and hasattr(block, 'text'):
143
+ content_blocks.append(block.text)
144
+ else:
145
+ # Handle other content types (tool_use, etc.)
146
+ try:
147
+ content_blocks.append(json.dumps(block.__dict__, ensure_ascii=False))
148
+ except:
149
+ content_blocks.append(str(block))
150
+
151
+ # Join all content blocks
152
+ if content_blocks:
153
+ output_response = '\n'.join(content_blocks)
154
+
155
+ # Apply truncation
156
+ if len(output_response) > Config.MAX_PROMPT_LENGTH:
157
+ marker = "...[TRUNCATED]"
158
+ marker_len = len(marker)
159
+ truncate_at = Config.MAX_PROMPT_LENGTH - marker_len
160
+ output_response = output_response[:truncate_at] + marker
161
+ was_truncated = True
162
+ logger.debug(
163
+ f"Output response truncated to "
164
+ f"{Config.MAX_PROMPT_LENGTH} characters"
165
+ )
166
+ except Exception as e:
167
+ logger.warning(f"Failed to extract response content: {e}")
168
+ output_response = None
169
+
170
+ return {
171
+ 'outputResponse': output_response,
172
+ 'promptsTruncated': was_truncated
173
+ }
174
+
175
+
176
+
177
+ # extract_streaming_response_content is imported from _core.prompt_extraction
178
+
@@ -0,0 +1,141 @@
1
+ """
2
+ Provider detection and configuration for AWS Bedrock support.
3
+
4
+ This module handles detection of AWS Bedrock vs standard Anthropic based on:
5
+ 1. Client instance type (boto3 bedrock-runtime client)
6
+ 2. Base URL substring matching ("amazonaws.com")
7
+ 3. Default to Anthropic
8
+
9
+ The detection is simple and focused on the MVP requirements.
10
+ """
11
+
12
+ import logging
13
+ import threading
14
+ from enum import Enum, auto
15
+ from typing import Optional, Any
16
+
17
+ logger = logging.getLogger("revenium_middleware.extension")
18
+
19
+
20
+ class Provider(Enum):
21
+ """Supported AI providers."""
22
+ ANTHROPIC = auto()
23
+ BEDROCK = auto()
24
+
25
+
26
+ def detect_provider(client: Optional[Any] = None, base_url: Optional[str] = None) -> Provider:
27
+ """
28
+ Detect which AI provider is being used based on available information.
29
+
30
+ Detection priority:
31
+ 1. Client instance type (boto3 bedrock-runtime) - most reliable
32
+ 2. Base URL substring matching ("amazonaws.com")
33
+ 3. Default to Anthropic
34
+
35
+ Args:
36
+ client: Client instance (may be boto3 bedrock-runtime client)
37
+ base_url: Base URL for API calls
38
+
39
+ Returns:
40
+ Provider enum indicating detected provider
41
+ """
42
+ logger.debug("Detecting AI provider...")
43
+
44
+ # 1. Check if client is boto3 bedrock-runtime client (most reliable)
45
+ if client and hasattr(client, "meta"):
46
+ try:
47
+ if hasattr(client.meta, "service_model") and \
48
+ client.meta.service_model.service_name == "bedrock-runtime":
49
+ logger.debug("Bedrock provider detected via boto3 client service_name")
50
+ return Provider.BEDROCK
51
+ except AttributeError:
52
+ # If meta doesn't have service_model, continue to next check
53
+ pass
54
+
55
+ # 2. Check base URL for AWS substring
56
+ if base_url and "amazonaws.com" in str(base_url).lower():
57
+ logger.debug(f"Bedrock provider detected via base_url: {base_url}")
58
+ return Provider.BEDROCK
59
+
60
+ # 3. Check for client base_url if not provided directly
61
+ if client and hasattr(client, 'base_url') and client.base_url:
62
+ if "amazonaws.com" in str(client.base_url).lower():
63
+ logger.debug(f"Bedrock provider detected via client.base_url: {client.base_url}")
64
+ return Provider.BEDROCK
65
+
66
+ # 4. Default to Anthropic
67
+ logger.debug("Defaulting to Anthropic provider")
68
+ return Provider.ANTHROPIC
69
+
70
+
71
+ def get_provider_metadata(provider: Provider) -> dict:
72
+ """
73
+ Get provider-specific metadata for usage records.
74
+
75
+ Args:
76
+ provider: Detected provider
77
+
78
+ Returns:
79
+ Dictionary with provider and model_source fields
80
+ """
81
+ if provider == Provider.BEDROCK:
82
+ return {
83
+ "provider": "AWS",
84
+ "model_source": "ANTHROPIC"
85
+ }
86
+ else: # ANTHROPIC
87
+ return {
88
+ "provider": "ANTHROPIC",
89
+ "model_source": "ANTHROPIC"
90
+ }
91
+
92
+
93
+ def is_bedrock_provider(provider: Provider) -> bool:
94
+ """
95
+ Check if the provider is AWS Bedrock.
96
+
97
+ Args:
98
+ provider: Provider to check
99
+
100
+ Returns:
101
+ True if AWS Bedrock, False otherwise
102
+ """
103
+ return provider == Provider.BEDROCK
104
+
105
+
106
+ # Thread-local storage for provider cache to ensure thread safety
107
+ _thread_local = threading.local()
108
+
109
+
110
+ def _get_thread_cache():
111
+ """Get thread-local cache, initializing if necessary."""
112
+ if not hasattr(_thread_local, 'detected_provider'):
113
+ _thread_local.detected_provider = None
114
+ _thread_local.detection_attempted = False
115
+ return _thread_local
116
+
117
+
118
+ def get_or_detect_provider(client: Optional[Any] = None, base_url: Optional[str] = None,
119
+ force_redetect: bool = False) -> Provider:
120
+ """
121
+ Get cached provider or detect if not already done.
122
+
123
+ This provides lazy loading - detection only happens when needed and is cached
124
+ per thread for thread safety.
125
+
126
+ Args:
127
+ client: Client instance
128
+ base_url: Base URL for API calls
129
+ force_redetect: Force re-detection even if cached
130
+
131
+ Returns:
132
+ Detected provider
133
+ """
134
+ cache = _get_thread_cache()
135
+
136
+ if force_redetect or not cache.detection_attempted:
137
+ cache.detected_provider = detect_provider(client, base_url)
138
+ cache.detection_attempted = True
139
+ logger.debug(f"Provider detection completed: {cache.detected_provider}")
140
+
141
+ return cache.detected_provider
@@ -0,0 +1,286 @@
1
+ """
2
+ Summary printer module for Revenium Anthropic middleware.
3
+
4
+ This module provides terminal summary output functionality that displays
5
+ cost and metrics information after each API request in either JSON or
6
+ human-readable format.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import time
12
+ from dataclasses import dataclass
13
+ from typing import Optional
14
+ import urllib.request
15
+ import urllib.error
16
+
17
+ from .config import (
18
+ Config,
19
+ get_base_url,
20
+ get_print_summary_config,
21
+ get_team_id,
22
+ )
23
+
24
+ logger = logging.getLogger("revenium_middleware.summary_printer")
25
+
26
+
27
+ @dataclass
28
+ class CompletionMetrics:
29
+ """Data class to hold cost information from the Revenium API."""
30
+
31
+ total_cost: Optional[float] = None
32
+
33
+
34
+ def fetch_completion_metrics(
35
+ transaction_id: str,
36
+ revenium_api_key: str,
37
+ ) -> Optional[CompletionMetrics]:
38
+ """
39
+ Fetch cost data from Revenium profitstream API.
40
+
41
+ Implements retry logic (3 attempts with 2-second delay).
42
+
43
+ Args:
44
+ transaction_id: The transaction ID to fetch metrics for
45
+ revenium_api_key: The Revenium API key for authentication
46
+
47
+ Returns:
48
+ CompletionMetrics if successful, None if fetch fails or cost not available
49
+ """
50
+ team_id = get_team_id()
51
+ if not team_id:
52
+ logger.debug("No team ID configured, skipping cost fetch")
53
+ return None
54
+
55
+ base_url = get_base_url()
56
+ url = (
57
+ f"{base_url}/profitstream/v2/api/sources/metrics/ai/completions"
58
+ f"?teamId={team_id}&transactionId={transaction_id}"
59
+ )
60
+
61
+ headers = {
62
+ "Authorization": f"Bearer {revenium_api_key}",
63
+ "Content-Type": "application/json",
64
+ }
65
+
66
+ for attempt in range(Config.SUMMARY_RETRY_ATTEMPTS):
67
+ try:
68
+ request = urllib.request.Request(url, headers=headers, method="GET")
69
+ with urllib.request.urlopen(
70
+ request, timeout=Config.SUMMARY_API_TIMEOUT
71
+ ) as response:
72
+ data = json.loads(response.read().decode("utf-8"))
73
+
74
+ # Extract cost from response
75
+ total_cost = data.get("totalCost")
76
+ if total_cost is not None:
77
+ return CompletionMetrics(total_cost=float(total_cost))
78
+
79
+ # Cost might be in a nested structure
80
+ if isinstance(data, dict):
81
+ # Try common field names
82
+ for field in ["cost", "total_cost", "totalCost"]:
83
+ if field in data and data[field] is not None:
84
+ return CompletionMetrics(total_cost=float(data[field]))
85
+
86
+ return CompletionMetrics(total_cost=None)
87
+
88
+ except urllib.error.HTTPError as e:
89
+ logger.debug(f"HTTP error fetching metrics (attempt {attempt + 1}): {e}")
90
+ except urllib.error.URLError as e:
91
+ logger.debug(f"URL error fetching metrics (attempt {attempt + 1}): {e}")
92
+ except json.JSONDecodeError as e:
93
+ logger.debug(f"JSON decode error (attempt {attempt + 1}): {e}")
94
+ except Exception as e:
95
+ logger.debug(f"Error fetching metrics (attempt {attempt + 1}): {e}")
96
+
97
+ # Wait before retry (except on last attempt)
98
+ if attempt < Config.SUMMARY_RETRY_ATTEMPTS - 1:
99
+ time.sleep(Config.SUMMARY_RETRY_DELAY)
100
+
101
+ return None
102
+
103
+
104
+ def format_and_print_json_summary(
105
+ model: str,
106
+ provider: str,
107
+ duration_seconds: float,
108
+ input_token_count: Optional[int],
109
+ output_token_count: Optional[int],
110
+ total_token_count: Optional[int],
111
+ cost: Optional[float],
112
+ cost_status: str,
113
+ trace_id: Optional[str] = None,
114
+ ) -> None:
115
+ """
116
+ Print single-line JSON output summary.
117
+
118
+ Args:
119
+ model: The model name used
120
+ provider: The provider name (e.g., ANTHROPIC, BEDROCK)
121
+ duration_seconds: Request duration in seconds
122
+ input_token_count: Number of input tokens
123
+ output_token_count: Number of output tokens
124
+ total_token_count: Total token count
125
+ cost: Total cost if available
126
+ cost_status: Status of cost retrieval
127
+ trace_id: Optional trace ID
128
+ """
129
+ summary = {
130
+ "model": model,
131
+ "provider": provider,
132
+ "durationSeconds": round(duration_seconds, 3),
133
+ "inputTokenCount": input_token_count,
134
+ "outputTokenCount": output_token_count,
135
+ "totalTokenCount": total_token_count,
136
+ "cost": cost,
137
+ "costStatus": cost_status,
138
+ }
139
+
140
+ if trace_id:
141
+ summary["traceId"] = trace_id
142
+
143
+ print(json.dumps(summary, separators=(",", ":")))
144
+
145
+
146
+ def format_and_print_human_summary(
147
+ model: str,
148
+ provider: str,
149
+ duration_seconds: float,
150
+ input_token_count: Optional[int],
151
+ output_token_count: Optional[int],
152
+ total_token_count: Optional[int],
153
+ cost: Optional[float],
154
+ cost_status: str,
155
+ trace_id: Optional[str] = None,
156
+ ) -> None:
157
+ """
158
+ Print professional human-readable output summary.
159
+
160
+ NO EMOJIS - professional format only.
161
+
162
+ Args:
163
+ model: The model name used
164
+ provider: The provider name (e.g., ANTHROPIC, BEDROCK)
165
+ duration_seconds: Request duration in seconds
166
+ input_token_count: Number of input tokens
167
+ output_token_count: Number of output tokens
168
+ total_token_count: Total token count
169
+ cost: Total cost if available
170
+ cost_status: Status of cost retrieval
171
+ trace_id: Optional trace ID
172
+ """
173
+ separator = "=" * 60
174
+
175
+ lines = [
176
+ separator,
177
+ "REVENIUM USAGE SUMMARY",
178
+ separator,
179
+ f"Model: {model}",
180
+ f"Provider: {provider}",
181
+ f"Duration: {duration_seconds:.2f}s",
182
+ "",
183
+ "Token Usage:",
184
+ f" Input Tokens: {input_token_count if input_token_count is not None else 'N/A'}",
185
+ f" Output Tokens: {output_token_count if output_token_count is not None else 'N/A'}",
186
+ f" Total Tokens: {total_token_count if total_token_count is not None else 'N/A'}",
187
+ "",
188
+ ]
189
+
190
+ # Add cost information
191
+ if cost is not None:
192
+ lines.append(f"Cost: ${cost:.6f}")
193
+ else:
194
+ lines.append(f"Cost: {cost_status}")
195
+
196
+ # Add trace ID if available
197
+ if trace_id:
198
+ lines.append("")
199
+ lines.append(f"Trace ID: {trace_id}")
200
+
201
+ lines.append(separator)
202
+
203
+ print("\n".join(lines))
204
+
205
+
206
+ def print_usage_summary(
207
+ model: str,
208
+ provider: str,
209
+ request_duration: float,
210
+ input_token_count: Optional[int],
211
+ output_token_count: Optional[int],
212
+ total_token_count: Optional[int],
213
+ transaction_id: str,
214
+ trace_id: Optional[str] = None,
215
+ revenium_api_key: Optional[str] = None,
216
+ ) -> None:
217
+ """
218
+ Main entry point for printing usage summary.
219
+
220
+ This is a fire-and-forget function - it never raises exceptions to the caller.
221
+
222
+ Args:
223
+ model: The model name used
224
+ provider: The provider name (e.g., ANTHROPIC, BEDROCK)
225
+ request_duration: Request duration in milliseconds
226
+ input_token_count: Number of input tokens
227
+ output_token_count: Number of output tokens
228
+ total_token_count: Total token count
229
+ transaction_id: The transaction ID for fetching cost
230
+ trace_id: Optional trace ID
231
+ revenium_api_key: The Revenium API key for fetching cost
232
+ """
233
+ try:
234
+ # Check if summary printing is enabled
235
+ summary_format = get_print_summary_config()
236
+ if summary_format is False:
237
+ return
238
+
239
+ # Convert duration from milliseconds to seconds
240
+ duration_seconds = request_duration / 1000.0
241
+
242
+ # Determine cost status and fetch cost if possible
243
+ cost: Optional[float] = None
244
+ cost_status: str = "unavailable"
245
+ team_id = get_team_id()
246
+
247
+ if revenium_api_key and team_id:
248
+ metrics = fetch_completion_metrics(transaction_id, revenium_api_key)
249
+ if metrics and metrics.total_cost is not None:
250
+ cost = metrics.total_cost
251
+ cost_status = "available"
252
+ else:
253
+ cost_status = "Pending (aggregating... check Revenium dashboard)"
254
+ elif not team_id:
255
+ cost_status = "Add REVENIUM_TEAM_ID to see pricing"
256
+
257
+ # Print in the appropriate format
258
+ if summary_format == "json":
259
+ format_and_print_json_summary(
260
+ model=model,
261
+ provider=provider,
262
+ duration_seconds=duration_seconds,
263
+ input_token_count=input_token_count,
264
+ output_token_count=output_token_count,
265
+ total_token_count=total_token_count,
266
+ cost=cost,
267
+ cost_status=cost_status,
268
+ trace_id=trace_id,
269
+ )
270
+ else:
271
+ # Default to human format
272
+ format_and_print_human_summary(
273
+ model=model,
274
+ provider=provider,
275
+ duration_seconds=duration_seconds,
276
+ input_token_count=input_token_count,
277
+ output_token_count=output_token_count,
278
+ total_token_count=total_token_count,
279
+ cost=cost,
280
+ cost_status=cost_status,
281
+ trace_id=trace_id,
282
+ )
283
+
284
+ except Exception as e:
285
+ # Fire-and-forget: never fail the main API call
286
+ logger.debug(f"Failed to print summary: {e}")