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,173 @@
1
+ """
2
+ Prompt extraction utilities for capturing AI prompts and responses.
3
+
4
+ This module provides functions to extract and truncate prompts from OpenAI 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 OpenAI API request.
23
+
24
+ Args:
25
+ kwargs: The kwargs dict passed to the OpenAI API call
26
+
27
+ Returns:
28
+ Dict containing:
29
+ - systemPrompt: String or None (system message content)
30
+ - inputMessages: JSON string or None (non-system messages)
31
+ - promptsTruncated: Boolean (True if any field was truncated)
32
+ """
33
+ messages = kwargs.get('messages', [])
34
+
35
+ if not messages:
36
+ return {
37
+ 'systemPrompt': None,
38
+ 'inputMessages': None,
39
+ 'promptsTruncated': False
40
+ }
41
+
42
+ system_prompt = None
43
+ user_messages = []
44
+ prompts_truncated = False
45
+
46
+ # Separate system message from other messages
47
+ for msg in messages:
48
+ if not isinstance(msg, dict):
49
+ continue
50
+
51
+ role = msg.get('role')
52
+ if role == 'system':
53
+ # Extract system prompt (take first system message if multiple)
54
+ if system_prompt is None:
55
+ content = msg.get('content', '')
56
+ if isinstance(content, str):
57
+ system_prompt = content
58
+ elif isinstance(content, list):
59
+ # Handle content as array (multimodal)
60
+ system_prompt = json.dumps(content)
61
+ else:
62
+ # Collect all non-system messages
63
+ user_messages.append(msg)
64
+
65
+ # Apply truncation to system prompt
66
+ # Keep total length at MAX_PROMPT_LENGTH by subtracting marker length
67
+ if system_prompt and len(system_prompt) > Config.MAX_PROMPT_LENGTH:
68
+ marker = "...[TRUNCATED]"
69
+ marker_len = len(marker)
70
+ truncate_at = Config.MAX_PROMPT_LENGTH - marker_len
71
+ system_prompt = system_prompt[:truncate_at] + marker
72
+ prompts_truncated = True
73
+ logger.debug(f"System prompt truncated to {Config.MAX_PROMPT_LENGTH} characters")
74
+
75
+ # Convert user messages to JSON string
76
+ input_messages = None
77
+ if user_messages:
78
+ try:
79
+ # First, truncate individual message contents to avoid invalid JSON
80
+ # when the total exceeds the limit
81
+ marker = "...[TRUNCATED]"
82
+ marker_len = len(marker)
83
+ truncated_messages = []
84
+ for msg in user_messages:
85
+ truncated_msg = msg.copy()
86
+ content = msg.get('content', '')
87
+ if isinstance(content, str) and len(content) > Config.MAX_PROMPT_LENGTH // 2:
88
+ # Truncate individual messages to half the limit to be safe
89
+ truncate_at = (Config.MAX_PROMPT_LENGTH // 2) - marker_len
90
+ truncated_msg['content'] = content[:truncate_at] + marker
91
+ prompts_truncated = True
92
+ truncated_messages.append(truncated_msg)
93
+
94
+ input_messages = json.dumps(truncated_messages, ensure_ascii=False)
95
+
96
+ # Apply final truncation if still too long (keeps valid JSON structure)
97
+ if len(input_messages) > Config.MAX_PROMPT_LENGTH:
98
+ truncate_at = Config.MAX_PROMPT_LENGTH - marker_len
99
+ input_messages = input_messages[:truncate_at] + marker
100
+ prompts_truncated = True
101
+ logger.debug(
102
+ f"Input messages truncated to "
103
+ f"{Config.MAX_PROMPT_LENGTH} characters"
104
+ )
105
+ except (TypeError, ValueError) as e:
106
+ logger.warning(f"Failed to serialize input messages to JSON: {e}")
107
+ input_messages = None
108
+
109
+ return {
110
+ 'systemPrompt': system_prompt,
111
+ 'inputMessages': input_messages,
112
+ 'promptsTruncated': prompts_truncated
113
+ }
114
+
115
+
116
+ def extract_response_content(response: Any, prompts_truncated: bool = False) -> Dict[str, Any]:
117
+ """
118
+ Extract output response content from OpenAI API response.
119
+
120
+ Args:
121
+ response: OpenAI API response object (ChatCompletion or similar)
122
+ prompts_truncated: Whether prompts were already truncated (from request)
123
+
124
+ Returns:
125
+ Dict containing:
126
+ - outputResponse: String or None (assistant response content)
127
+ - promptsTruncated: Boolean (True if any field was truncated)
128
+ """
129
+ output_response = None
130
+ was_truncated = prompts_truncated
131
+
132
+ try:
133
+ # Extract content from response.choices[0].message.content
134
+ if hasattr(response, 'choices') and response.choices and len(response.choices) > 0:
135
+ first_choice = response.choices[0]
136
+
137
+ if hasattr(first_choice, 'message') and hasattr(first_choice.message, 'content'):
138
+ content = first_choice.message.content
139
+
140
+ if content:
141
+ if isinstance(content, str):
142
+ output_response = content
143
+ else:
144
+ # Handle non-string content (e.g., structured output)
145
+ output_response = json.dumps(content, ensure_ascii=False)
146
+
147
+ # Apply truncation - keep total at MAX_PROMPT_LENGTH
148
+ if len(output_response) > Config.MAX_PROMPT_LENGTH:
149
+ marker = "...[TRUNCATED]"
150
+ marker_len = len(marker)
151
+ truncate_at = Config.MAX_PROMPT_LENGTH - marker_len
152
+ output_response = (
153
+ output_response[:truncate_at]
154
+ + marker
155
+ )
156
+ was_truncated = True
157
+ logger.debug(
158
+ f"Output response truncated to "
159
+ f"{Config.MAX_PROMPT_LENGTH} characters"
160
+ )
161
+ except Exception as e:
162
+ logger.warning(f"Failed to extract response content: {e}")
163
+ output_response = None
164
+
165
+ return {
166
+ 'outputResponse': output_response,
167
+ 'promptsTruncated': was_truncated
168
+ }
169
+
170
+
171
+
172
+ # extract_streaming_response_content is imported from _core.prompt_extraction
173
+
@@ -0,0 +1,170 @@
1
+ """
2
+ Provider detection and configuration for Azure OpenAI support.
3
+
4
+ This module handles detection of Azure OpenAI vs standard OpenAI based on:
5
+ 1. Environment variables (AZURE_OPENAI_ENDPOINT)
6
+ 2. Base URL substring matching ("azure")
7
+ 3. Client instance type detection
8
+
9
+ The detection is lazy-loaded and only impacts Azure users.
10
+ """
11
+
12
+ import os
13
+ import logging
14
+ import threading
15
+ from enum import Enum, auto
16
+ from typing import Optional, Any
17
+
18
+ logger = logging.getLogger("revenium_middleware.extension")
19
+
20
+
21
+ class Provider(Enum):
22
+ """Supported AI providers."""
23
+ OPENAI = auto()
24
+ AZURE_OPENAI = auto()
25
+ OLLAMA = auto() # Existing provider support
26
+
27
+
28
+ def detect_provider(client: Optional[Any] = None, base_url: Optional[str] = None) -> Provider:
29
+ """
30
+ Detect which AI provider is being used based on available information.
31
+
32
+ Detection priority:
33
+ 1. Client instance type (AzureOpenAI) - most reliable
34
+ 2. Base URL substring matching ("azure")
35
+ 3. Environment variables (AZURE_OPENAI_ENDPOINT) - only if client suggests Azure
36
+ 4. Default to OpenAI
37
+
38
+ Args:
39
+ client: OpenAI client instance (may be AzureOpenAI)
40
+ base_url: Base URL for API calls
41
+
42
+ Returns:
43
+ Provider enum indicating detected provider
44
+ """
45
+ logger.debug("Detecting AI provider...")
46
+
47
+ # 1. Check client instance type first (most reliable)
48
+ if client and hasattr(client, '__class__'):
49
+ client_class_name = client.__class__.__name__
50
+ if "Azure" in client_class_name:
51
+ logger.debug(f"Azure provider detected via client type: {client_class_name}")
52
+ return Provider.AZURE_OPENAI
53
+
54
+ # 2. Check base URL for Azure substring (broader than just azure.com)
55
+ if base_url and "azure" in str(base_url).lower():
56
+ logger.debug(f"Azure provider detected via base_url substring: {base_url}")
57
+ return Provider.AZURE_OPENAI
58
+
59
+ # 3. Check for client base_url if not provided directly
60
+ if client and hasattr(client, 'base_url') and client.base_url:
61
+ if "azure" in str(client.base_url).lower():
62
+ logger.debug(f"Azure provider detected via client.base_url: {client.base_url}")
63
+ return Provider.AZURE_OPENAI
64
+
65
+ # 4. Check for OLLAMA via base URL patterns
66
+ if base_url and ("localhost:11434" in str(base_url) or "ollama" in str(base_url).lower()):
67
+ logger.debug(f"OLLAMA provider detected via base_url: {base_url}")
68
+ return Provider.OLLAMA
69
+
70
+ if client and hasattr(client, 'base_url') and client.base_url:
71
+ client_url = str(client.base_url).lower()
72
+ if "localhost:11434" in client_url or "ollama" in client_url:
73
+ logger.debug(f"OLLAMA provider detected via client.base_url: {client.base_url}")
74
+ return Provider.OLLAMA
75
+
76
+ # 5. Check environment variables only if we have some indication this might be Azure
77
+ # (This prevents false positives when both Azure and OpenAI configs are present)
78
+ if client and hasattr(client, '__class__') and "Azure" in str(type(client)):
79
+ azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
80
+ if azure_endpoint:
81
+ logger.debug(f"Azure provider detected via AZURE_OPENAI_ENDPOINT: {azure_endpoint}")
82
+ return Provider.AZURE_OPENAI
83
+
84
+ # 6. Default to OpenAI
85
+ logger.debug("Defaulting to OpenAI provider")
86
+ return Provider.OPENAI
87
+
88
+
89
+ def get_provider_metadata(provider: Provider) -> dict:
90
+ """
91
+ Get provider-specific metadata for usage records.
92
+
93
+ Args:
94
+ provider: Detected provider
95
+
96
+ Returns:
97
+ Dictionary with provider and model_source fields
98
+ """
99
+ if provider == Provider.AZURE_OPENAI:
100
+ return {
101
+ "provider": "Azure",
102
+ "model_source": "OPENAI"
103
+ }
104
+ elif provider == Provider.OLLAMA:
105
+ return {
106
+ "provider": "OLLAMA",
107
+ "model_source": "OLLAMA"
108
+ }
109
+ else: # OPENAI
110
+ return {
111
+ "provider": "OPENAI",
112
+ "model_source": "OPENAI"
113
+ }
114
+
115
+
116
+ def is_azure_provider(provider: Provider) -> bool:
117
+ """
118
+ Check if the provider is Azure OpenAI.
119
+
120
+ Args:
121
+ provider: Provider to check
122
+
123
+ Returns:
124
+ True if Azure OpenAI, False otherwise
125
+ """
126
+ return provider == Provider.AZURE_OPENAI
127
+
128
+
129
+ # Global provider cache to avoid repeated detection
130
+ _detected_provider: Optional[Provider] = None
131
+ _provider_detection_attempted: bool = False
132
+ _provider_lock = threading.Lock() # Thread safety for global state
133
+
134
+
135
+ def get_or_detect_provider(client: Optional[Any] = None, base_url: Optional[str] = None,
136
+ force_redetect: bool = False) -> Provider:
137
+ """
138
+ Get cached provider or detect if not already done.
139
+
140
+ This provides lazy loading - detection only happens when needed and is cached.
141
+ Thread-safe implementation prevents race conditions in multi-threaded environments.
142
+
143
+ Args:
144
+ client: OpenAI client instance
145
+ base_url: Base URL for API calls
146
+ force_redetect: Force re-detection even if cached
147
+
148
+ Returns:
149
+ Detected provider
150
+ """
151
+ global _detected_provider, _provider_detection_attempted
152
+
153
+ # Thread-safe provider detection with double-checked locking pattern
154
+ if force_redetect or not _provider_detection_attempted:
155
+ with _provider_lock:
156
+ # Double-check inside the lock to prevent race conditions
157
+ if force_redetect or not _provider_detection_attempted:
158
+ _detected_provider = detect_provider(client, base_url)
159
+ _provider_detection_attempted = True
160
+ logger.debug(f"Provider detection completed: {_detected_provider}")
161
+
162
+ return _detected_provider
163
+
164
+
165
+ def reset_provider_cache():
166
+ """Reset provider detection cache. Useful for testing. Thread-safe implementation."""
167
+ global _detected_provider, _provider_detection_attempted
168
+ with _provider_lock:
169
+ _detected_provider = None
170
+ _provider_detection_attempted = False
@@ -0,0 +1,292 @@
1
+ """
2
+ Terminal summary printer for Revenium OpenAI middleware.
3
+
4
+ This module provides functionality to print cost/metrics summaries to the terminal
5
+ after each API request. Supports both human-readable and JSON output formats.
6
+
7
+ Fetches cost data from Revenium's traces API and formats for console display.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import time
13
+ from typing import Optional, Dict, Any
14
+ from urllib.parse import urlencode
15
+ from urllib.request import Request, urlopen
16
+ from urllib.error import URLError, HTTPError
17
+
18
+ from .config import (
19
+ Config,
20
+ SummaryFormat,
21
+ get_print_summary_config,
22
+ get_team_id,
23
+ get_base_url,
24
+ )
25
+
26
+ logger = logging.getLogger("revenium_middleware.summary_printer")
27
+
28
+
29
+ class CompletionMetrics:
30
+ """Completion metrics from Revenium API."""
31
+
32
+ def __init__(self, total_cost: Optional[float] = None):
33
+ self.total_cost = total_cost
34
+
35
+
36
+ def fetch_completion_metrics(
37
+ transaction_id: str,
38
+ revenium_api_key: str,
39
+ ) -> Optional[CompletionMetrics]:
40
+ """
41
+ Fetch metrics from Revenium completions API.
42
+
43
+ Args:
44
+ transaction_id: The transaction ID to fetch metrics for
45
+ revenium_api_key: Revenium API key for authentication
46
+
47
+ Returns:
48
+ CompletionMetrics if successful, None otherwise
49
+ """
50
+ team_id = get_team_id()
51
+ if not team_id:
52
+ logger.debug(
53
+ "Team ID not configured, skipping cost retrieval for summary"
54
+ )
55
+ return None
56
+
57
+ base_url = get_base_url().rstrip("/")
58
+ # Note: profitstream API uses a different path structure than the metering API
59
+ url = f"{base_url}/profitstream/v2/api/sources/metrics/ai/completions"
60
+ params = {
61
+ "teamId": team_id,
62
+ "transactionId": transaction_id,
63
+ }
64
+ url_with_params = f"{url}?{urlencode(params)}"
65
+
66
+ logger.debug(f"Fetching completion metrics from {url_with_params}")
67
+
68
+ max_retries = Config.SUMMARY_RETRY_ATTEMPTS
69
+ retry_delay = Config.SUMMARY_RETRY_DELAY
70
+
71
+ for attempt in range(max_retries):
72
+ try:
73
+ request = Request(url_with_params)
74
+ request.add_header("Authorization", revenium_api_key)
75
+ request.add_header("Content-Type", "application/json")
76
+
77
+ with urlopen(request, timeout=Config.SUMMARY_API_TIMEOUT) as response:
78
+ if response.status == 200:
79
+ data = json.loads(response.read().decode())
80
+ embedded = data.get("_embedded", {})
81
+ metrics_list = embedded.get("aICompletionMetricResourceList", [])
82
+
83
+ if metrics_list:
84
+ first_metric = metrics_list[0]
85
+ total_cost = first_metric.get("totalCost")
86
+ logger.debug(f"Retrieved cost: {total_cost}")
87
+ return CompletionMetrics(total_cost=total_cost)
88
+
89
+ logger.debug(
90
+ f"No metrics found yet (attempt {attempt + 1}/{max_retries})"
91
+ )
92
+
93
+ if attempt < max_retries - 1:
94
+ logger.debug(
95
+ f"Waiting for metrics to aggregate "
96
+ f"(attempt {attempt + 1}/{max_retries})..."
97
+ )
98
+ time.sleep(retry_delay)
99
+
100
+ except (HTTPError, URLError) as e:
101
+ logger.debug(
102
+ f"Failed to fetch trace metrics: {e} "
103
+ f"(attempt {attempt + 1}/{max_retries})"
104
+ )
105
+ if attempt < max_retries - 1:
106
+ time.sleep(retry_delay)
107
+ except Exception as e:
108
+ logger.debug(
109
+ f"Unexpected error fetching metrics: {e} "
110
+ f"(attempt {attempt + 1}/{max_retries})"
111
+ )
112
+ if attempt < max_retries - 1:
113
+ time.sleep(retry_delay)
114
+
115
+ return None
116
+
117
+
118
+ def format_and_print_json_summary(
119
+ model: str,
120
+ provider: str,
121
+ duration_seconds: float,
122
+ input_token_count: Optional[int],
123
+ output_token_count: Optional[int],
124
+ total_token_count: Optional[int],
125
+ trace_id: Optional[str],
126
+ metrics: Optional[CompletionMetrics],
127
+ ) -> None:
128
+ """
129
+ Format and print summary in JSON format.
130
+
131
+ Args:
132
+ model: Model name
133
+ provider: Provider name
134
+ duration_seconds: Request duration in seconds
135
+ input_token_count: Input token count
136
+ output_token_count: Output token count
137
+ total_token_count: Total token count
138
+ trace_id: Trace ID
139
+ metrics: Optional completion metrics from API
140
+ """
141
+ team_id = get_team_id()
142
+
143
+ summary: Dict[str, Any] = {
144
+ "model": model,
145
+ "provider": provider,
146
+ "durationSeconds": round(duration_seconds, 2),
147
+ "inputTokenCount": input_token_count,
148
+ "outputTokenCount": output_token_count,
149
+ "totalTokenCount": total_token_count,
150
+ "cost": metrics.total_cost if metrics and metrics.total_cost is not None else None,
151
+ }
152
+
153
+ # Add cost status if cost is null
154
+ if summary["cost"] is None:
155
+ summary["costStatus"] = "pending" if team_id else "unavailable"
156
+
157
+ # Add trace ID if present
158
+ if trace_id:
159
+ summary["traceId"] = trace_id
160
+
161
+ print(json.dumps(summary))
162
+
163
+
164
+ def format_and_print_human_summary(
165
+ model: str,
166
+ provider: str,
167
+ duration_seconds: float,
168
+ input_token_count: Optional[int],
169
+ output_token_count: Optional[int],
170
+ total_token_count: Optional[int],
171
+ trace_id: Optional[str],
172
+ metrics: Optional[CompletionMetrics],
173
+ ) -> None:
174
+ """
175
+ Format and print summary in human-readable format.
176
+
177
+ Args:
178
+ model: Model name
179
+ provider: Provider name
180
+ duration_seconds: Request duration in seconds
181
+ input_token_count: Input token count
182
+ output_token_count: Output token count
183
+ total_token_count: Total token count
184
+ trace_id: Trace ID
185
+ metrics: Optional completion metrics from API
186
+ """
187
+ team_id = get_team_id()
188
+
189
+ print("=" * 60)
190
+ print("REVENIUM USAGE SUMMARY")
191
+ print("=" * 60)
192
+ print(f"Model: {model}")
193
+ print(f"Provider: {provider}")
194
+ print(f"Duration: {duration_seconds:.2f}s")
195
+
196
+ print("\nToken Usage:")
197
+ print(f" Input Tokens: {(input_token_count or 0):,}")
198
+ print(f" Output Tokens: {(output_token_count or 0):,}")
199
+ print(f" Total Tokens: {(total_token_count or 0):,}")
200
+
201
+ if metrics and metrics.total_cost is not None:
202
+ print(f"\nCost: ${metrics.total_cost:.6f}")
203
+ else:
204
+ if team_id:
205
+ print(
206
+ "\nCost: Pending (aggregating... check Revenium dashboard)"
207
+ )
208
+ else:
209
+ print(
210
+ "\nCost: Add REVENIUM_TEAM_ID to see pricing "
211
+ "(find your team ID in the Revenium web app)"
212
+ )
213
+
214
+ if trace_id:
215
+ print(f"\nTrace ID: {trace_id}")
216
+
217
+ print("=" * 60 + "\n")
218
+
219
+
220
+ def print_usage_summary(
221
+ model: str,
222
+ provider: str,
223
+ request_duration: int,
224
+ input_token_count: Optional[int],
225
+ output_token_count: Optional[int],
226
+ total_token_count: Optional[int],
227
+ transaction_id: Optional[str],
228
+ trace_id: Optional[str],
229
+ revenium_api_key: str,
230
+ ) -> None:
231
+ """
232
+ Print usage summary to console (fire-and-forget).
233
+
234
+ This function is called after tracking is complete to optionally display
235
+ a formatted cost/metrics summary in the terminal.
236
+
237
+ Args:
238
+ model: Model name
239
+ provider: Provider name
240
+ request_duration: Request duration in milliseconds
241
+ input_token_count: Input token count
242
+ output_token_count: Output token count
243
+ total_token_count: Total token count
244
+ transaction_id: Transaction ID for fetching metrics
245
+ trace_id: Trace ID for display
246
+ revenium_api_key: Revenium API key for authentication
247
+ """
248
+ print_summary = get_print_summary_config()
249
+ if not print_summary:
250
+ return
251
+
252
+ # Determine format
253
+ format_type: SummaryFormat = "human" if print_summary is True else print_summary
254
+
255
+ duration_seconds = request_duration / 1000.0
256
+
257
+ # Fetch metrics if team_id and transaction_id are available
258
+ metrics: Optional[CompletionMetrics] = None
259
+ team_id = get_team_id()
260
+ if team_id and transaction_id:
261
+ try:
262
+ metrics = fetch_completion_metrics(transaction_id, revenium_api_key)
263
+ except Exception as e:
264
+ logger.debug(f"Failed to fetch metrics: {e}")
265
+
266
+ # Print summary in the appropriate format
267
+ try:
268
+ if format_type == "json":
269
+ format_and_print_json_summary(
270
+ model=model,
271
+ provider=provider,
272
+ duration_seconds=duration_seconds,
273
+ input_token_count=input_token_count,
274
+ output_token_count=output_token_count,
275
+ total_token_count=total_token_count,
276
+ trace_id=trace_id,
277
+ metrics=metrics,
278
+ )
279
+ else:
280
+ format_and_print_human_summary(
281
+ model=model,
282
+ provider=provider,
283
+ duration_seconds=duration_seconds,
284
+ input_token_count=input_token_count,
285
+ output_token_count=output_token_count,
286
+ total_token_count=total_token_count,
287
+ trace_id=trace_id,
288
+ metrics=metrics,
289
+ )
290
+ except Exception as e:
291
+ logger.debug(f"Failed to format and print summary: {e}")
292
+