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.
- revenium_middleware/__init__.py +184 -0
- revenium_middleware/_core/__init__.py +65 -0
- revenium_middleware/_core/config.py +165 -0
- revenium_middleware/_core/context.py +109 -0
- revenium_middleware/_core/decorators.py +202 -0
- revenium_middleware/_core/metering.py +207 -0
- revenium_middleware/_core/prompt_extraction.py +55 -0
- revenium_middleware/_core/subscriber.py +51 -0
- revenium_middleware/_core/trace_fields.py +265 -0
- revenium_middleware/anthropic/__init__.py +108 -0
- revenium_middleware/anthropic/bedrock_adapter.py +753 -0
- revenium_middleware/anthropic/config.py +29 -0
- revenium_middleware/anthropic/middleware.py +1070 -0
- revenium_middleware/anthropic/prompt_extractor.py +178 -0
- revenium_middleware/anthropic/provider.py +141 -0
- revenium_middleware/anthropic/summary_printer.py +286 -0
- revenium_middleware/anthropic/trace_fields.py +158 -0
- revenium_middleware/google/__init__.py +114 -0
- revenium_middleware/google/common/__init__.py +127 -0
- revenium_middleware/google/common/exceptions.py +137 -0
- revenium_middleware/google/common/protocols.py +192 -0
- revenium_middleware/google/common/summary_printer.py +271 -0
- revenium_middleware/google/common/trace_fields.py +205 -0
- revenium_middleware/google/common/types.py +208 -0
- revenium_middleware/google/common/utils.py +1111 -0
- revenium_middleware/google/config.py +64 -0
- revenium_middleware/google/google_ai/__init__.py +53 -0
- revenium_middleware/google/google_ai/middleware.py +667 -0
- revenium_middleware/google/google_ai/provider.py +135 -0
- revenium_middleware/google/prompt_extractor.py +396 -0
- revenium_middleware/google/vertex_ai/__init__.py +56 -0
- revenium_middleware/google/vertex_ai/middleware.py +1162 -0
- revenium_middleware/google/vertex_ai/provider.py +99 -0
- revenium_middleware/litellm/__init__.py +25 -0
- revenium_middleware/litellm/client/__init__.py +81 -0
- revenium_middleware/litellm/client/config.py +53 -0
- revenium_middleware/litellm/client/context.py +198 -0
- revenium_middleware/litellm/client/decorators.py +912 -0
- revenium_middleware/litellm/client/hooks.py +192 -0
- revenium_middleware/litellm/client/integrations/__init__.py +26 -0
- revenium_middleware/litellm/client/integrations/crewai.py +446 -0
- revenium_middleware/litellm/client/middleware.py +321 -0
- revenium_middleware/litellm/client/summary_printer.py +314 -0
- revenium_middleware/litellm/client/trace_fields.py +51 -0
- revenium_middleware/litellm/client/validation.py +207 -0
- revenium_middleware/litellm/proxy/__init__.py +25 -0
- revenium_middleware/litellm/proxy/middleware.py +217 -0
- revenium_middleware/ollama/__init__.py +28 -0
- revenium_middleware/ollama/middleware.py +569 -0
- revenium_middleware/ollama/trace_fields.py +63 -0
- revenium_middleware/openai/__init__.py +23 -0
- revenium_middleware/openai/azure_config.py +169 -0
- revenium_middleware/openai/azure_model_resolver.py +219 -0
- revenium_middleware/openai/config.py +45 -0
- revenium_middleware/openai/exceptions.py +115 -0
- revenium_middleware/openai/langchain/__init__.py +114 -0
- revenium_middleware/openai/langchain/_utils.py +129 -0
- revenium_middleware/openai/langchain/unified_handler.py +526 -0
- revenium_middleware/openai/middleware.py +1451 -0
- revenium_middleware/openai/prompt_extractor.py +173 -0
- revenium_middleware/openai/provider.py +170 -0
- revenium_middleware/openai/summary_printer.py +292 -0
- revenium_middleware/openai/trace_fields.py +98 -0
- revenium_middleware/perplexity/__init__.py +97 -0
- revenium_middleware/perplexity/middleware.py +379 -0
- revenium_middleware/perplexity/perplexity_sdk.py +256 -0
- revenium_middleware/perplexity/provider.py +84 -0
- revenium_middleware/perplexity/trace_fields.py +25 -0
- revenium_python_sdk-0.1.0.dist-info/METADATA +252 -0
- revenium_python_sdk-0.1.0.dist-info/RECORD +73 -0
- revenium_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- revenium_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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}")
|