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,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protocol definitions for Google AI API responses.
|
|
3
|
+
|
|
4
|
+
This module defines Protocol classes that describe the expected structure
|
|
5
|
+
of API responses from Google AI and Vertex AI SDKs, improving type safety
|
|
6
|
+
and enabling better static analysis.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, Optional, List, Any, Union
|
|
10
|
+
from typing_extensions import runtime_checkable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class UsageMetadataProtocol(Protocol):
|
|
15
|
+
"""Protocol for usage metadata in API responses."""
|
|
16
|
+
|
|
17
|
+
# Google AI SDK attributes
|
|
18
|
+
prompt_token_count: Optional[int]
|
|
19
|
+
candidates_token_count: Optional[int]
|
|
20
|
+
total_token_count: Optional[int]
|
|
21
|
+
cached_content_token_count: Optional[int]
|
|
22
|
+
|
|
23
|
+
# Alternative attribute names (for compatibility)
|
|
24
|
+
prompt_tokens: Optional[int]
|
|
25
|
+
completion_tokens: Optional[int]
|
|
26
|
+
total_tokens: Optional[int]
|
|
27
|
+
cached_tokens: Optional[int]
|
|
28
|
+
input_tokens: Optional[int]
|
|
29
|
+
output_tokens: Optional[int]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@runtime_checkable
|
|
33
|
+
class CandidateProtocol(Protocol):
|
|
34
|
+
"""Protocol for candidate objects in API responses."""
|
|
35
|
+
|
|
36
|
+
finish_reason: Optional[str]
|
|
37
|
+
content: Optional[Any]
|
|
38
|
+
text: Optional[str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class EmbeddingProtocol(Protocol):
|
|
43
|
+
"""Protocol for embedding objects."""
|
|
44
|
+
|
|
45
|
+
values: List[float]
|
|
46
|
+
statistics: Optional[Any]
|
|
47
|
+
_prediction_response: Optional[Any]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@runtime_checkable
|
|
51
|
+
class EmbeddingStatisticsProtocol(Protocol):
|
|
52
|
+
"""Protocol for embedding statistics."""
|
|
53
|
+
|
|
54
|
+
token_count: Optional[int]
|
|
55
|
+
billableCharacterCount: Optional[int]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@runtime_checkable
|
|
59
|
+
class ChatResponseProtocol(Protocol):
|
|
60
|
+
"""Protocol for chat/generation API responses."""
|
|
61
|
+
|
|
62
|
+
# Model information
|
|
63
|
+
model: Optional[str]
|
|
64
|
+
model_name: Optional[str]
|
|
65
|
+
model_version: Optional[str]
|
|
66
|
+
_model_name: Optional[str]
|
|
67
|
+
|
|
68
|
+
# Usage information
|
|
69
|
+
usage: Optional[UsageMetadataProtocol]
|
|
70
|
+
usage_metadata: Optional[UsageMetadataProtocol]
|
|
71
|
+
|
|
72
|
+
# Response content
|
|
73
|
+
text: Optional[str]
|
|
74
|
+
content: Optional[str]
|
|
75
|
+
candidates: Optional[List[CandidateProtocol]]
|
|
76
|
+
|
|
77
|
+
# Completion information
|
|
78
|
+
finish_reason: Optional[str]
|
|
79
|
+
stop_reason: Optional[str]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@runtime_checkable
|
|
83
|
+
class EmbeddingResponseProtocol(Protocol):
|
|
84
|
+
"""Protocol for embedding API responses."""
|
|
85
|
+
|
|
86
|
+
# Model information
|
|
87
|
+
model: Optional[str]
|
|
88
|
+
model_name: Optional[str]
|
|
89
|
+
|
|
90
|
+
# Embeddings data
|
|
91
|
+
embeddings: Optional[List[EmbeddingProtocol]]
|
|
92
|
+
values: Optional[List[float]] # For single embedding responses
|
|
93
|
+
|
|
94
|
+
# Usage information
|
|
95
|
+
usage: Optional[UsageMetadataProtocol]
|
|
96
|
+
usage_metadata: Optional[UsageMetadataProtocol]
|
|
97
|
+
statistics: Optional[EmbeddingStatisticsProtocol]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@runtime_checkable
|
|
101
|
+
class StreamChunkProtocol(Protocol):
|
|
102
|
+
"""Protocol for streaming response chunks."""
|
|
103
|
+
|
|
104
|
+
# Model information
|
|
105
|
+
model: Optional[str]
|
|
106
|
+
model_version: Optional[str]
|
|
107
|
+
|
|
108
|
+
# Content
|
|
109
|
+
text: Optional[str]
|
|
110
|
+
content: Optional[str]
|
|
111
|
+
candidates: Optional[List[CandidateProtocol]]
|
|
112
|
+
|
|
113
|
+
# Usage information (typically in final chunk)
|
|
114
|
+
usage_metadata: Optional[UsageMetadataProtocol]
|
|
115
|
+
|
|
116
|
+
# Completion information
|
|
117
|
+
finish_reason: Optional[str]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@runtime_checkable
|
|
121
|
+
class ClientProtocol(Protocol):
|
|
122
|
+
"""Protocol for Google AI client objects."""
|
|
123
|
+
|
|
124
|
+
_vertexai: Optional[bool]
|
|
125
|
+
_api_client: Optional[Any]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Type guards for runtime type checking
|
|
129
|
+
def is_chat_response(response: Any) -> bool:
|
|
130
|
+
"""Check if response matches ChatResponseProtocol."""
|
|
131
|
+
return isinstance(response, ChatResponseProtocol)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_embedding_response(response: Any) -> bool:
|
|
135
|
+
"""Check if response matches EmbeddingResponseProtocol."""
|
|
136
|
+
return isinstance(response, EmbeddingResponseProtocol)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def is_stream_chunk(chunk: Any) -> bool:
|
|
140
|
+
"""Check if chunk matches StreamChunkProtocol."""
|
|
141
|
+
return isinstance(chunk, StreamChunkProtocol)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def has_usage_metadata(response: Any) -> bool:
|
|
145
|
+
"""Check if response has usage metadata."""
|
|
146
|
+
return (
|
|
147
|
+
hasattr(response, "usage_metadata") and response.usage_metadata is not None
|
|
148
|
+
) or (hasattr(response, "usage") and response.usage is not None)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def has_token_counts(usage: Any) -> bool:
|
|
152
|
+
"""Check if usage object has token count information."""
|
|
153
|
+
if not usage:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# Check for any token count attributes
|
|
157
|
+
token_attrs = [
|
|
158
|
+
"total_token_count",
|
|
159
|
+
"total_tokens",
|
|
160
|
+
"prompt_token_count",
|
|
161
|
+
"prompt_tokens",
|
|
162
|
+
"input_tokens",
|
|
163
|
+
"candidates_token_count",
|
|
164
|
+
"completion_tokens",
|
|
165
|
+
"output_tokens",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
return any(
|
|
169
|
+
hasattr(usage, attr) and getattr(usage, attr, 0) > 0 for attr in token_attrs
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Utility functions for safe attribute access
|
|
174
|
+
def safe_getattr(obj: Any, attr: str, default: Any = None) -> Any:
|
|
175
|
+
"""Safely get attribute with fallback to default."""
|
|
176
|
+
try:
|
|
177
|
+
return getattr(obj, attr, default)
|
|
178
|
+
except (AttributeError, TypeError):
|
|
179
|
+
return default
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_token_count(usage: Any, attr_names: List[str]) -> int:
|
|
183
|
+
"""Get token count from usage object, trying multiple attribute names."""
|
|
184
|
+
if not usage:
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
for attr_name in attr_names:
|
|
188
|
+
value = safe_getattr(usage, attr_name, 0)
|
|
189
|
+
if isinstance(value, int) and value > 0:
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
return 0
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Summary printer for terminal output of API usage metrics.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to display cost and metrics information
|
|
5
|
+
after each API request in human-readable or JSON format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
import urllib.parse
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from . import trace_fields
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class CompletionMetrics:
|
|
24
|
+
"""Data class to hold cost information from the Revenium API."""
|
|
25
|
+
|
|
26
|
+
total_cost: Optional[float] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fetch_completion_metrics(
|
|
30
|
+
transaction_id: str,
|
|
31
|
+
revenium_api_key: str,
|
|
32
|
+
) -> Optional[CompletionMetrics]:
|
|
33
|
+
"""
|
|
34
|
+
Fetch cost data from Revenium profitstream API.
|
|
35
|
+
|
|
36
|
+
Implements retry logic (3 attempts with 1-second delay).
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
transaction_id: The transaction ID to fetch metrics for
|
|
40
|
+
revenium_api_key: The Revenium API key for authentication
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
CompletionMetrics with cost data, or None if fetch fails
|
|
44
|
+
"""
|
|
45
|
+
team_id = trace_fields.get_team_id()
|
|
46
|
+
if not team_id:
|
|
47
|
+
logger.debug("No REVENIUM_TEAM_ID set, skipping cost fetch")
|
|
48
|
+
return CompletionMetrics(total_cost=None)
|
|
49
|
+
|
|
50
|
+
base_url = trace_fields.get_base_url()
|
|
51
|
+
# Use urlencode to properly encode query parameters
|
|
52
|
+
params = urllib.parse.urlencode({
|
|
53
|
+
'teamId': team_id,
|
|
54
|
+
'transactionId': transaction_id
|
|
55
|
+
})
|
|
56
|
+
url = (
|
|
57
|
+
f"{base_url}/profitstream/v2/api/sources/metrics/ai/completions"
|
|
58
|
+
f"?{params}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
headers = {
|
|
62
|
+
"Authorization": f"Bearer {revenium_api_key}",
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for attempt in range(trace_fields.SUMMARY_RETRY_ATTEMPTS):
|
|
67
|
+
try:
|
|
68
|
+
response = requests.get(
|
|
69
|
+
url,
|
|
70
|
+
headers=headers,
|
|
71
|
+
timeout=trace_fields.SUMMARY_API_TIMEOUT,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if response.status_code == 200:
|
|
75
|
+
data = response.json()
|
|
76
|
+
# Check both camelCase and snake_case
|
|
77
|
+
# Don't use 'or' to avoid treating 0.0 as falsy
|
|
78
|
+
total_cost = data.get("totalCost")
|
|
79
|
+
if total_cost is None:
|
|
80
|
+
total_cost = data.get("total_cost")
|
|
81
|
+
return CompletionMetrics(total_cost=total_cost)
|
|
82
|
+
else:
|
|
83
|
+
logger.debug(
|
|
84
|
+
f"Metrics API returned status {response.status_code} "
|
|
85
|
+
f"on attempt {attempt + 1}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
except requests.exceptions.RequestException as e:
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"Failed to fetch metrics on attempt {attempt + 1}: {e}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Wait before retry (except on last attempt)
|
|
94
|
+
if attempt < trace_fields.SUMMARY_RETRY_ATTEMPTS - 1:
|
|
95
|
+
time.sleep(trace_fields.SUMMARY_RETRY_DELAY)
|
|
96
|
+
|
|
97
|
+
logger.debug(
|
|
98
|
+
f"Failed to fetch metrics after {trace_fields.SUMMARY_RETRY_ATTEMPTS} attempts"
|
|
99
|
+
)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def format_and_print_json_summary(
|
|
104
|
+
model: str,
|
|
105
|
+
provider: str,
|
|
106
|
+
duration_seconds: float,
|
|
107
|
+
input_token_count: Optional[int],
|
|
108
|
+
output_token_count: Optional[int],
|
|
109
|
+
total_token_count: Optional[int],
|
|
110
|
+
cost: Optional[float],
|
|
111
|
+
trace_id: Optional[str],
|
|
112
|
+
) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Print single-line JSON output for machine-readable summary.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
model: Model name used
|
|
118
|
+
provider: Provider name (e.g., "GOOGLE")
|
|
119
|
+
duration_seconds: Request duration in seconds
|
|
120
|
+
input_token_count: Number of input tokens
|
|
121
|
+
output_token_count: Number of output tokens
|
|
122
|
+
total_token_count: Total token count
|
|
123
|
+
cost: Cost in dollars (or None if unavailable)
|
|
124
|
+
trace_id: Trace ID for the request
|
|
125
|
+
"""
|
|
126
|
+
summary = {
|
|
127
|
+
"model": model,
|
|
128
|
+
"provider": provider,
|
|
129
|
+
"durationSeconds": round(duration_seconds, 3),
|
|
130
|
+
"inputTokenCount": input_token_count,
|
|
131
|
+
"outputTokenCount": output_token_count,
|
|
132
|
+
"totalTokenCount": total_token_count,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if cost is not None:
|
|
136
|
+
summary["cost"] = cost
|
|
137
|
+
summary["costStatus"] = "available"
|
|
138
|
+
else:
|
|
139
|
+
summary["costStatus"] = "unavailable"
|
|
140
|
+
|
|
141
|
+
if trace_id:
|
|
142
|
+
summary["traceId"] = trace_id
|
|
143
|
+
|
|
144
|
+
print(json.dumps(summary))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def format_and_print_human_summary(
|
|
148
|
+
model: str,
|
|
149
|
+
provider: str,
|
|
150
|
+
duration_seconds: float,
|
|
151
|
+
input_token_count: Optional[int],
|
|
152
|
+
output_token_count: Optional[int],
|
|
153
|
+
total_token_count: Optional[int],
|
|
154
|
+
cost: Optional[float],
|
|
155
|
+
trace_id: Optional[str],
|
|
156
|
+
team_id: Optional[str],
|
|
157
|
+
) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Print professional human-readable output.
|
|
160
|
+
|
|
161
|
+
NO EMOJIS - professional format only.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
model: Model name used
|
|
165
|
+
provider: Provider name (e.g., "GOOGLE")
|
|
166
|
+
duration_seconds: Request duration in seconds
|
|
167
|
+
input_token_count: Number of input tokens
|
|
168
|
+
output_token_count: Number of output tokens
|
|
169
|
+
total_token_count: Total token count
|
|
170
|
+
cost: Cost in dollars (or None if unavailable)
|
|
171
|
+
trace_id: Trace ID for the request
|
|
172
|
+
team_id: Team ID (used for cost status message)
|
|
173
|
+
"""
|
|
174
|
+
separator = "=" * 60
|
|
175
|
+
|
|
176
|
+
print(separator)
|
|
177
|
+
print("REVENIUM USAGE SUMMARY")
|
|
178
|
+
print(separator)
|
|
179
|
+
print(f"Model: {model}")
|
|
180
|
+
print(f"Provider: {provider}")
|
|
181
|
+
print(f"Duration: {duration_seconds:.2f}s")
|
|
182
|
+
print()
|
|
183
|
+
print("Token Usage:")
|
|
184
|
+
print(f" Input Tokens: {input_token_count or 0}")
|
|
185
|
+
print(f" Output Tokens: {output_token_count or 0}")
|
|
186
|
+
print(f" Total Tokens: {total_token_count or 0}")
|
|
187
|
+
print()
|
|
188
|
+
|
|
189
|
+
if cost is not None:
|
|
190
|
+
print(f"Cost: ${cost:.6f}")
|
|
191
|
+
elif team_id:
|
|
192
|
+
print("Cost: Pending (aggregating... check Revenium dashboard)")
|
|
193
|
+
else:
|
|
194
|
+
print("Cost: Add REVENIUM_TEAM_ID to see pricing")
|
|
195
|
+
|
|
196
|
+
if trace_id:
|
|
197
|
+
print()
|
|
198
|
+
print(f"Trace ID: {trace_id}")
|
|
199
|
+
|
|
200
|
+
print(separator)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def print_usage_summary(
|
|
204
|
+
model: str,
|
|
205
|
+
provider: str,
|
|
206
|
+
request_duration: int,
|
|
207
|
+
input_token_count: Optional[int],
|
|
208
|
+
output_token_count: Optional[int],
|
|
209
|
+
total_token_count: Optional[int],
|
|
210
|
+
transaction_id: str,
|
|
211
|
+
trace_id: Optional[str],
|
|
212
|
+
revenium_api_key: str,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Main entry point for printing usage summary.
|
|
216
|
+
|
|
217
|
+
Fire-and-forget: Wrapped in try/except to never fail the main API call.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
model: Model name used
|
|
221
|
+
provider: Provider name (e.g., "Google")
|
|
222
|
+
request_duration: Request duration in milliseconds
|
|
223
|
+
input_token_count: Number of input tokens
|
|
224
|
+
output_token_count: Number of output tokens
|
|
225
|
+
total_token_count: Total token count
|
|
226
|
+
transaction_id: Transaction ID for fetching metrics
|
|
227
|
+
trace_id: Trace ID for the request
|
|
228
|
+
revenium_api_key: Revenium API key for authentication
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
# Check if summary output is enabled
|
|
232
|
+
summary_format = trace_fields.get_print_summary_config()
|
|
233
|
+
if summary_format is False:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# Convert duration from milliseconds to seconds
|
|
237
|
+
duration_seconds = request_duration / 1000.0
|
|
238
|
+
|
|
239
|
+
# Attempt to fetch cost metrics
|
|
240
|
+
metrics = fetch_completion_metrics(transaction_id, revenium_api_key)
|
|
241
|
+
cost = metrics.total_cost if metrics else None
|
|
242
|
+
team_id = trace_fields.get_team_id()
|
|
243
|
+
|
|
244
|
+
if summary_format == "json":
|
|
245
|
+
format_and_print_json_summary(
|
|
246
|
+
model=model,
|
|
247
|
+
provider=provider,
|
|
248
|
+
duration_seconds=duration_seconds,
|
|
249
|
+
input_token_count=input_token_count,
|
|
250
|
+
output_token_count=output_token_count,
|
|
251
|
+
total_token_count=total_token_count,
|
|
252
|
+
cost=cost,
|
|
253
|
+
trace_id=trace_id,
|
|
254
|
+
)
|
|
255
|
+
else: # "human"
|
|
256
|
+
format_and_print_human_summary(
|
|
257
|
+
model=model,
|
|
258
|
+
provider=provider,
|
|
259
|
+
duration_seconds=duration_seconds,
|
|
260
|
+
input_token_count=input_token_count,
|
|
261
|
+
output_token_count=output_token_count,
|
|
262
|
+
total_token_count=total_token_count,
|
|
263
|
+
cost=cost,
|
|
264
|
+
trace_id=trace_id,
|
|
265
|
+
team_id=team_id,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
# Fire-and-forget: Never fail the main operation
|
|
270
|
+
logger.debug(f"Failed to print summary: {e}")
|
|
271
|
+
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace visualization field capture and validation.
|
|
3
|
+
|
|
4
|
+
This module provides functions to capture trace visualization fields from
|
|
5
|
+
environment variables and validate them according to the specification.
|
|
6
|
+
|
|
7
|
+
Shared functions are imported from _core.trace_fields. This module retains
|
|
8
|
+
only Google-specific functions: detect_vision_content and detect_operation_type.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Optional, Dict, Any, Union, Literal
|
|
13
|
+
|
|
14
|
+
from revenium_middleware._core.config import (
|
|
15
|
+
Config as _CoreConfig,
|
|
16
|
+
SummaryFormat,
|
|
17
|
+
parse_print_summary_value,
|
|
18
|
+
get_print_summary_config,
|
|
19
|
+
get_team_id,
|
|
20
|
+
get_base_url,
|
|
21
|
+
)
|
|
22
|
+
from revenium_middleware._core.trace_fields import ( # noqa: F401 — re-exported
|
|
23
|
+
TRACE_TYPE_MAX_LENGTH,
|
|
24
|
+
TRACE_NAME_MAX_LENGTH,
|
|
25
|
+
TRACE_TYPE_PATTERN,
|
|
26
|
+
ENV_REVENIUM_ENVIRONMENT,
|
|
27
|
+
ENV_ENVIRONMENT,
|
|
28
|
+
ENV_DEPLOYMENT_ENV,
|
|
29
|
+
ENV_REVENIUM_REGION,
|
|
30
|
+
ENV_AWS_REGION,
|
|
31
|
+
ENV_AWS_DEFAULT_REGION,
|
|
32
|
+
ENV_AZURE_REGION,
|
|
33
|
+
ENV_GCP_REGION,
|
|
34
|
+
ENV_GOOGLE_CLOUD_REGION,
|
|
35
|
+
ENV_REVENIUM_CREDENTIAL_ALIAS,
|
|
36
|
+
ENV_REVENIUM_TRACE_TYPE,
|
|
37
|
+
ENV_REVENIUM_TRACE_NAME,
|
|
38
|
+
ENV_REVENIUM_PARENT_TRANSACTION_ID,
|
|
39
|
+
ENV_REVENIUM_TRANSACTION_NAME,
|
|
40
|
+
ENV_REVENIUM_RETRY_NUMBER,
|
|
41
|
+
get_environment,
|
|
42
|
+
get_region,
|
|
43
|
+
get_credential_alias,
|
|
44
|
+
get_trace_type,
|
|
45
|
+
get_trace_name,
|
|
46
|
+
get_parent_transaction_id,
|
|
47
|
+
get_transaction_name,
|
|
48
|
+
get_retry_number,
|
|
49
|
+
validate_trace_type,
|
|
50
|
+
validate_trace_name,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
# Summary output configuration (re-exported from core)
|
|
56
|
+
ENV_REVENIUM_PRINT_SUMMARY = _CoreConfig.ENV_REVENIUM_PRINT_SUMMARY
|
|
57
|
+
ENV_REVENIUM_TEAM_ID = _CoreConfig.ENV_REVENIUM_TEAM_ID
|
|
58
|
+
ENV_REVENIUM_METERING_BASE_URL = _CoreConfig.ENV_REVENIUM_BASE_URL
|
|
59
|
+
|
|
60
|
+
SUMMARY_RETRY_ATTEMPTS = _CoreConfig.SUMMARY_RETRY_ATTEMPTS
|
|
61
|
+
SUMMARY_RETRY_DELAY: float = 1.0 # Google uses 1.0s
|
|
62
|
+
SUMMARY_API_TIMEOUT = _CoreConfig.SUMMARY_API_TIMEOUT
|
|
63
|
+
DEFAULT_BASE_URL = _CoreConfig.DEFAULT_BASE_URL
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def detect_vision_content(contents: Any = None) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Detect if contents contain vision/image content.
|
|
69
|
+
|
|
70
|
+
Google AI content formats that indicate vision input:
|
|
71
|
+
- Part with inline_data containing image/* or video/* MIME type
|
|
72
|
+
- Part with file_data containing image/* or video/* MIME type
|
|
73
|
+
- PIL Image objects passed directly
|
|
74
|
+
- google.genai types.Part with inline_data/file_data
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
contents: The contents parameter from generate_content() call.
|
|
78
|
+
Can be a string, list, Part, or Content object.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if any content contains image/video data, False otherwise.
|
|
82
|
+
"""
|
|
83
|
+
if contents is None:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# Normalize to a list for uniform processing
|
|
87
|
+
if not isinstance(contents, list):
|
|
88
|
+
contents = [contents]
|
|
89
|
+
|
|
90
|
+
for item in contents:
|
|
91
|
+
if _item_has_vision_content(item, _depth=0):
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_MAX_VISION_RECURSION_DEPTH = 10
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _item_has_vision_content(item: Any, _depth: int = 0) -> bool:
|
|
101
|
+
"""Check if a single content item contains vision data."""
|
|
102
|
+
if item is None or _depth > _MAX_VISION_RECURSION_DEPTH:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
# Check for PIL Image objects
|
|
106
|
+
try:
|
|
107
|
+
import PIL.Image
|
|
108
|
+
if isinstance(item, PIL.Image.Image):
|
|
109
|
+
return True
|
|
110
|
+
except ImportError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# Check for dict-based content (raw API format)
|
|
114
|
+
if isinstance(item, dict):
|
|
115
|
+
# Check inline_data
|
|
116
|
+
inline_data = item.get("inline_data")
|
|
117
|
+
if inline_data and isinstance(inline_data, dict):
|
|
118
|
+
mime_type = inline_data.get("mime_type", "")
|
|
119
|
+
if mime_type.startswith("image/") or mime_type.startswith("video/"):
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# Check file_data
|
|
123
|
+
file_data = item.get("file_data")
|
|
124
|
+
if file_data and isinstance(file_data, dict):
|
|
125
|
+
mime_type = file_data.get("mime_type", "")
|
|
126
|
+
if mime_type.startswith("image/") or mime_type.startswith("video/"):
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
# Check parts list within a Content dict
|
|
130
|
+
parts = item.get("parts")
|
|
131
|
+
if parts and isinstance(parts, list):
|
|
132
|
+
for part in parts:
|
|
133
|
+
if _item_has_vision_content(part, _depth + 1):
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
# Check type field (e.g., {"type": "image", ...})
|
|
137
|
+
if item.get("type") in ("image", "video"):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
# Check for google.genai types (Part objects with inline_data/file_data attributes)
|
|
143
|
+
if hasattr(item, "inline_data") and item.inline_data is not None:
|
|
144
|
+
mime_type = getattr(item.inline_data, "mime_type", "") or ""
|
|
145
|
+
if mime_type.startswith("image/") or mime_type.startswith("video/"):
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
if hasattr(item, "file_data") and item.file_data is not None:
|
|
149
|
+
mime_type = getattr(item.file_data, "mime_type", "") or ""
|
|
150
|
+
if mime_type.startswith("image/") or mime_type.startswith("video/"):
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
# Check Content objects with parts
|
|
154
|
+
if hasattr(item, "parts") and item.parts:
|
|
155
|
+
parts = item.parts if isinstance(item.parts, list) else [item.parts]
|
|
156
|
+
for part in parts:
|
|
157
|
+
if _item_has_vision_content(part, _depth + 1):
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
# Check for bytes (raw image data passed directly)
|
|
161
|
+
if isinstance(item, bytes) and len(item) > 0:
|
|
162
|
+
# Check for common image magic bytes
|
|
163
|
+
if (item[:4] == b'\x89PNG' or # PNG
|
|
164
|
+
item[:2] == b'\xff\xd8' or # JPEG
|
|
165
|
+
item[:4] == b'GIF8' or # GIF
|
|
166
|
+
(len(item) >= 12 and item[:4] == b'RIFF' and item[8:12] == b'WEBP')): # WebP
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def detect_operation_type(
|
|
173
|
+
method_name: str,
|
|
174
|
+
request_body: Optional[Dict[str, Any]] = None
|
|
175
|
+
) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Auto-detect operation type from method name and request.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
method_name: API method name (e.g., 'generate_content', 'embed_content')
|
|
181
|
+
request_body: Optional request body to check for tools
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Operation type string ('CHAT', 'GENERATE', 'EMBED', 'TOOL_CALL')
|
|
185
|
+
"""
|
|
186
|
+
request_body = request_body or {}
|
|
187
|
+
|
|
188
|
+
# Embeddings
|
|
189
|
+
if 'embed' in method_name.lower():
|
|
190
|
+
return 'EMBED'
|
|
191
|
+
|
|
192
|
+
# Content generation (chat/generate)
|
|
193
|
+
if 'generate' in method_name.lower() or 'chat' in method_name.lower():
|
|
194
|
+
# Check for tools in request
|
|
195
|
+
has_tools = request_body.get('tools')
|
|
196
|
+
if has_tools:
|
|
197
|
+
return 'TOOL_CALL'
|
|
198
|
+
return 'CHAT'
|
|
199
|
+
|
|
200
|
+
# Default fallback
|
|
201
|
+
return 'CHAT'
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# parse_print_summary_value, get_print_summary_config, get_team_id, get_base_url
|
|
205
|
+
# are imported from revenium_middleware._core.config at the top of this file
|