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,1111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utilities for Google AI middleware.
|
|
3
|
+
|
|
4
|
+
This module contains shared functionality used by both Google AI SDK
|
|
5
|
+
and Vertex AI SDK middleware implementations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
from revenium_middleware import client, run_async_in_thread, shutdown_event
|
|
15
|
+
|
|
16
|
+
from .types import UsageData, OperationType, ProviderMetadata, TokenCounts
|
|
17
|
+
from .exceptions import MeteringError, APIResponseError, safe_extract
|
|
18
|
+
from .protocols import has_token_counts, safe_getattr, get_token_count
|
|
19
|
+
from . import trace_fields
|
|
20
|
+
from .summary_printer import print_usage_summary
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("revenium_middleware.extension")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_debug_logging_enabled() -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Check if debug logging is currently enabled for Revenium middleware.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
bool: True if debug logging is enabled, False otherwise
|
|
31
|
+
"""
|
|
32
|
+
# Check environment variable first
|
|
33
|
+
log_level_str = os.getenv("REVENIUM_LOG_LEVEL", "INFO").upper()
|
|
34
|
+
if log_level_str == "DEBUG":
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
# Check actual logger level as fallback
|
|
38
|
+
revenium_logger = logging.getLogger("revenium_middleware")
|
|
39
|
+
return revenium_logger.getEffectiveLevel() <= logging.DEBUG
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def generate_transaction_id() -> str:
|
|
43
|
+
"""Generate a unique transaction ID."""
|
|
44
|
+
return str(uuid.uuid4())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_timestamp(dt: datetime.datetime) -> str:
|
|
48
|
+
"""Format datetime as ISO string for API calls."""
|
|
49
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_field_with_fallback(
|
|
53
|
+
usage_metadata: Dict[str, Any],
|
|
54
|
+
new_snake: str,
|
|
55
|
+
new_camel: str,
|
|
56
|
+
old_snake: str,
|
|
57
|
+
old_camel: str,
|
|
58
|
+
field_label: str,
|
|
59
|
+
) -> Optional[str]:
|
|
60
|
+
"""
|
|
61
|
+
Extract field with 4-level fallback and deprecation warning.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
usage_metadata: Dictionary containing usage metadata
|
|
65
|
+
new_snake: New snake_case field name (e.g., "organization_name")
|
|
66
|
+
new_camel: New camelCase field name (e.g., "organizationName")
|
|
67
|
+
old_snake: Old snake_case field name (e.g., "organization_id")
|
|
68
|
+
old_camel: Old camelCase field name (e.g., "organizationId")
|
|
69
|
+
field_label: Human-readable label for the field (e.g., "organization")
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The field value with precedence: new_snake > new_camel > old_snake > old_camel
|
|
73
|
+
"""
|
|
74
|
+
# Extract with precedence: new_snake > new_camel > old_snake > old_camel
|
|
75
|
+
value = usage_metadata.get(new_snake)
|
|
76
|
+
if value is None:
|
|
77
|
+
value = usage_metadata.get(new_camel)
|
|
78
|
+
if value is None:
|
|
79
|
+
value = usage_metadata.get(old_snake)
|
|
80
|
+
if value is None:
|
|
81
|
+
value = usage_metadata.get(old_camel)
|
|
82
|
+
|
|
83
|
+
# Log deprecation warning if old fields are used without new fields
|
|
84
|
+
if usage_metadata.get(old_snake) or usage_metadata.get(old_camel):
|
|
85
|
+
if not (usage_metadata.get(new_snake) or usage_metadata.get(new_camel)):
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"Fields '{old_camel}' and '{old_snake}' are deprecated. "
|
|
88
|
+
f"Use '{new_camel}' or '{new_snake}' instead. "
|
|
89
|
+
"The old fields will be removed in a future version."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def calculate_duration_ms(
|
|
96
|
+
start_time: datetime.datetime, end_time: datetime.datetime
|
|
97
|
+
) -> int:
|
|
98
|
+
"""Calculate duration in milliseconds between two timestamps."""
|
|
99
|
+
return int((end_time - start_time).total_seconds() * 1000)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def log_token_usage(
|
|
103
|
+
transaction_id: str,
|
|
104
|
+
model: str,
|
|
105
|
+
prompt_tokens: int,
|
|
106
|
+
completion_tokens: int,
|
|
107
|
+
total_tokens: int,
|
|
108
|
+
cached_tokens: int,
|
|
109
|
+
stop_reason: str,
|
|
110
|
+
request_time: str,
|
|
111
|
+
response_time: str,
|
|
112
|
+
request_duration: int,
|
|
113
|
+
usage_metadata: Dict[str, Any],
|
|
114
|
+
provider: str = "Google",
|
|
115
|
+
model_source: str = "GOOGLE",
|
|
116
|
+
is_streamed: bool = False,
|
|
117
|
+
time_to_first_token: int = 0,
|
|
118
|
+
operation_type: OperationType = OperationType.CHAT,
|
|
119
|
+
# Prompt capture fields
|
|
120
|
+
system_prompt: Optional[str] = None,
|
|
121
|
+
input_messages: Optional[str] = None,
|
|
122
|
+
output_response: Optional[str] = None,
|
|
123
|
+
prompts_truncated: Optional[bool] = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Log token usage to Revenium.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
transaction_id: Unique identifier for this API call
|
|
130
|
+
model: Model name used for the request
|
|
131
|
+
prompt_tokens: Number of input tokens
|
|
132
|
+
completion_tokens: Number of output tokens
|
|
133
|
+
total_tokens: Total token count
|
|
134
|
+
cached_tokens: Number of cached tokens used
|
|
135
|
+
stop_reason: Reason the generation stopped
|
|
136
|
+
request_time: ISO timestamp of request start
|
|
137
|
+
response_time: ISO timestamp of response completion
|
|
138
|
+
request_duration: Duration in milliseconds
|
|
139
|
+
usage_metadata: Additional metadata for the request
|
|
140
|
+
provider: Provider name (always "Google")
|
|
141
|
+
model_source: Model source (always "GOOGLE")
|
|
142
|
+
is_streamed: Whether this was a streaming response
|
|
143
|
+
time_to_first_token: Time to first token in milliseconds
|
|
144
|
+
operation_type: Type of operation (CHAT or EMBED)
|
|
145
|
+
system_prompt: System instruction/prompt text
|
|
146
|
+
input_messages: Input messages as JSON string
|
|
147
|
+
output_response: Output response text
|
|
148
|
+
prompts_truncated: Whether any prompts were truncated
|
|
149
|
+
"""
|
|
150
|
+
if shutdown_event.is_set():
|
|
151
|
+
logger.warning("Skipping metering call during shutdown")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
logger.debug(
|
|
155
|
+
"Metering call to Revenium for %s operation %s",
|
|
156
|
+
operation_type.lower(),
|
|
157
|
+
transaction_id,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Prepare arguments for create_completion (using snake_case for Python client library)
|
|
161
|
+
completion_args = {
|
|
162
|
+
"cache_creation_token_count": cached_tokens,
|
|
163
|
+
"cache_read_token_count": 0,
|
|
164
|
+
"input_token_cost": None, # Let backend calculate from model pricing
|
|
165
|
+
"output_token_cost": None, # Let backend calculate from model pricing
|
|
166
|
+
"total_cost": None, # Let backend calculate from model pricing
|
|
167
|
+
"output_token_count": completion_tokens,
|
|
168
|
+
"cost_type": "AI",
|
|
169
|
+
"model": model,
|
|
170
|
+
"input_token_count": prompt_tokens,
|
|
171
|
+
"provider": provider,
|
|
172
|
+
"model_source": model_source,
|
|
173
|
+
"reasoning_token_count": 0,
|
|
174
|
+
"request_time": request_time,
|
|
175
|
+
"response_time": response_time,
|
|
176
|
+
"completion_start_time": response_time,
|
|
177
|
+
"request_duration": int(request_duration),
|
|
178
|
+
"stop_reason": stop_reason,
|
|
179
|
+
"total_token_count": total_tokens,
|
|
180
|
+
"transaction_id": transaction_id,
|
|
181
|
+
"is_streamed": is_streamed,
|
|
182
|
+
"operation_type": operation_type.value, # Convert enum to string
|
|
183
|
+
"time_to_first_token": time_to_first_token,
|
|
184
|
+
"middleware_source": "python", # Required parameter for Google Python middleware
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Add optional metadata fields if they exist (using snake_case for Python client)
|
|
188
|
+
if usage_metadata.get("trace_id"):
|
|
189
|
+
completion_args["trace_id"] = usage_metadata.get("trace_id")
|
|
190
|
+
if usage_metadata.get("task_type"):
|
|
191
|
+
completion_args["task_type"] = usage_metadata.get("task_type")
|
|
192
|
+
|
|
193
|
+
# Extract organization and product names with deprecation warnings
|
|
194
|
+
organization_name = extract_field_with_fallback(
|
|
195
|
+
usage_metadata,
|
|
196
|
+
"organization_name",
|
|
197
|
+
"organizationName",
|
|
198
|
+
"organization_id",
|
|
199
|
+
"organizationId",
|
|
200
|
+
"organization",
|
|
201
|
+
)
|
|
202
|
+
product_name = extract_field_with_fallback(
|
|
203
|
+
usage_metadata,
|
|
204
|
+
"product_name",
|
|
205
|
+
"productName",
|
|
206
|
+
"product_id",
|
|
207
|
+
"productId",
|
|
208
|
+
"product",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Add to completion_args using new field names
|
|
212
|
+
if organization_name:
|
|
213
|
+
completion_args["organization_name"] = organization_name
|
|
214
|
+
|
|
215
|
+
if product_name:
|
|
216
|
+
completion_args["product_name"] = product_name
|
|
217
|
+
|
|
218
|
+
if usage_metadata.get("subscription_id"):
|
|
219
|
+
completion_args["subscription_id"] = usage_metadata.get("subscription_id")
|
|
220
|
+
if usage_metadata.get("agent"):
|
|
221
|
+
completion_args["agent"] = usage_metadata.get("agent")
|
|
222
|
+
if usage_metadata.get("response_quality_score"):
|
|
223
|
+
completion_args["response_quality_score"] = usage_metadata.get(
|
|
224
|
+
"response_quality_score"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Add vision content flag if detected
|
|
228
|
+
if usage_metadata.get("has_vision_content"):
|
|
229
|
+
completion_args["has_vision_content"] = True
|
|
230
|
+
|
|
231
|
+
# Add trace visualization fields (v0.2.0+)
|
|
232
|
+
# These fields support both environment variables and usage_metadata parameters
|
|
233
|
+
# Priority: usage_metadata > environment variable
|
|
234
|
+
|
|
235
|
+
# Environment field
|
|
236
|
+
environment = (
|
|
237
|
+
usage_metadata.get("environment") or
|
|
238
|
+
trace_fields.get_environment()
|
|
239
|
+
)
|
|
240
|
+
if environment:
|
|
241
|
+
completion_args["environment"] = environment
|
|
242
|
+
|
|
243
|
+
# Region field
|
|
244
|
+
region = (
|
|
245
|
+
usage_metadata.get("region") or
|
|
246
|
+
trace_fields.get_region()
|
|
247
|
+
)
|
|
248
|
+
if region:
|
|
249
|
+
completion_args["region"] = region
|
|
250
|
+
|
|
251
|
+
# Credential alias field
|
|
252
|
+
credential_alias = (
|
|
253
|
+
usage_metadata.get("credential_alias") or
|
|
254
|
+
usage_metadata.get("credentialAlias") or
|
|
255
|
+
trace_fields.get_credential_alias()
|
|
256
|
+
)
|
|
257
|
+
if credential_alias:
|
|
258
|
+
completion_args["credential_alias"] = credential_alias
|
|
259
|
+
|
|
260
|
+
# Trace type field
|
|
261
|
+
trace_type = (
|
|
262
|
+
usage_metadata.get("trace_type") or
|
|
263
|
+
usage_metadata.get("traceType") or
|
|
264
|
+
trace_fields.get_trace_type()
|
|
265
|
+
)
|
|
266
|
+
if trace_type:
|
|
267
|
+
# Validate if coming from usage_metadata
|
|
268
|
+
if usage_metadata.get("trace_type") or usage_metadata.get("traceType"):
|
|
269
|
+
trace_type = trace_fields.validate_trace_type(trace_type)
|
|
270
|
+
if trace_type:
|
|
271
|
+
completion_args["trace_type"] = trace_type
|
|
272
|
+
|
|
273
|
+
# Trace name field
|
|
274
|
+
trace_name = (
|
|
275
|
+
usage_metadata.get("trace_name") or
|
|
276
|
+
usage_metadata.get("traceName") or
|
|
277
|
+
trace_fields.get_trace_name()
|
|
278
|
+
)
|
|
279
|
+
if trace_name:
|
|
280
|
+
# Validate if coming from usage_metadata
|
|
281
|
+
if usage_metadata.get("trace_name") or usage_metadata.get("traceName"):
|
|
282
|
+
trace_name = trace_fields.validate_trace_name(trace_name)
|
|
283
|
+
if trace_name:
|
|
284
|
+
completion_args["trace_name"] = trace_name
|
|
285
|
+
|
|
286
|
+
# Parent transaction ID field
|
|
287
|
+
parent_transaction_id = (
|
|
288
|
+
usage_metadata.get("parent_transaction_id") or
|
|
289
|
+
usage_metadata.get("parentTransactionId") or
|
|
290
|
+
trace_fields.get_parent_transaction_id()
|
|
291
|
+
)
|
|
292
|
+
if parent_transaction_id:
|
|
293
|
+
completion_args["parent_transaction_id"] = parent_transaction_id
|
|
294
|
+
|
|
295
|
+
# Transaction name field (with fallback to task_type)
|
|
296
|
+
transaction_name = trace_fields.get_transaction_name(usage_metadata)
|
|
297
|
+
if transaction_name:
|
|
298
|
+
completion_args["transaction_name"] = transaction_name
|
|
299
|
+
|
|
300
|
+
# Retry number field
|
|
301
|
+
retry_number = trace_fields.get_retry_number()
|
|
302
|
+
if retry_number > 0:
|
|
303
|
+
completion_args["retry_number"] = retry_number
|
|
304
|
+
|
|
305
|
+
# Build subscriber object - support both nested and flat formats
|
|
306
|
+
subscriber_data = {}
|
|
307
|
+
flat_keys_used = []
|
|
308
|
+
|
|
309
|
+
# Prefer nested format if present (recommended structure)
|
|
310
|
+
if "subscriber" in usage_metadata:
|
|
311
|
+
nested_subscriber = usage_metadata["subscriber"]
|
|
312
|
+
if isinstance(nested_subscriber, dict):
|
|
313
|
+
# Use nested structure directly
|
|
314
|
+
subscriber_data = nested_subscriber.copy()
|
|
315
|
+
logger.debug("Using nested subscriber format (recommended)")
|
|
316
|
+
else:
|
|
317
|
+
# Fall back to flat keys for backward compatibility
|
|
318
|
+
subscriber_id = usage_metadata.get("subscriber_id")
|
|
319
|
+
subscriber_email = usage_metadata.get("subscriber_email")
|
|
320
|
+
credential_name = usage_metadata.get("subscriber_credential_name")
|
|
321
|
+
credential_value = usage_metadata.get("subscriber_credential")
|
|
322
|
+
|
|
323
|
+
if subscriber_id:
|
|
324
|
+
subscriber_data["id"] = subscriber_id
|
|
325
|
+
flat_keys_used.append("subscriber_id")
|
|
326
|
+
if subscriber_email:
|
|
327
|
+
subscriber_data["email"] = subscriber_email
|
|
328
|
+
flat_keys_used.append("subscriber_email")
|
|
329
|
+
|
|
330
|
+
# Add credential sub-object if credential data is provided
|
|
331
|
+
credential_data = {}
|
|
332
|
+
if credential_name:
|
|
333
|
+
credential_data["name"] = credential_name
|
|
334
|
+
flat_keys_used.append("subscriber_credential_name")
|
|
335
|
+
if credential_value:
|
|
336
|
+
credential_data["value"] = credential_value
|
|
337
|
+
flat_keys_used.append("subscriber_credential")
|
|
338
|
+
|
|
339
|
+
if credential_data:
|
|
340
|
+
subscriber_data["credential"] = credential_data
|
|
341
|
+
|
|
342
|
+
# Log deprecation warning if flat keys were used
|
|
343
|
+
if flat_keys_used:
|
|
344
|
+
logger.warning(
|
|
345
|
+
f"Flat subscriber keys are deprecated: {flat_keys_used}. "
|
|
346
|
+
"Please use nested 'subscriber' object format: "
|
|
347
|
+
"{'subscriber': {'id': '...', 'email': '...', 'credential': {'name': '...', 'value': '...'}}}"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Only add subscriber to completion_args if we have subscriber data
|
|
351
|
+
if subscriber_data:
|
|
352
|
+
completion_args["subscriber"] = subscriber_data
|
|
353
|
+
|
|
354
|
+
# Add prompt capture fields only if they have values
|
|
355
|
+
if system_prompt is not None:
|
|
356
|
+
completion_args["system_prompt"] = system_prompt
|
|
357
|
+
if input_messages is not None:
|
|
358
|
+
completion_args["input_messages"] = input_messages
|
|
359
|
+
if output_response is not None:
|
|
360
|
+
completion_args["output_response"] = output_response
|
|
361
|
+
if prompts_truncated is not None:
|
|
362
|
+
completion_args["prompts_truncated"] = prompts_truncated
|
|
363
|
+
|
|
364
|
+
# Log the arguments at debug level (redact sensitive prompt data)
|
|
365
|
+
safe_args = {k: v for k, v in completion_args.items()
|
|
366
|
+
if k not in ('system_prompt', 'input_messages', 'output_response')}
|
|
367
|
+
if 'system_prompt' in completion_args:
|
|
368
|
+
safe_args['system_prompt'] = '[REDACTED]'
|
|
369
|
+
if 'input_messages' in completion_args:
|
|
370
|
+
safe_args['input_messages'] = '[REDACTED]'
|
|
371
|
+
if 'output_response' in completion_args:
|
|
372
|
+
safe_args['output_response'] = '[REDACTED]'
|
|
373
|
+
logger.debug("Calling client.ai.create_completion with args: %s", safe_args)
|
|
374
|
+
|
|
375
|
+
# Debug logging for metering call
|
|
376
|
+
logger.debug(
|
|
377
|
+
f"Metering call for {operation_type.value}: {transaction_id}, tokens: {prompt_tokens}+{completion_tokens}={total_tokens}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
# The client.ai.create_completion method is not async, so don't use await
|
|
382
|
+
result = client.ai.create_completion(**completion_args)
|
|
383
|
+
logger.debug("Metering call result: %s", result)
|
|
384
|
+
logger.info(" REVENIUM SUCCESS: Metering call successful: %s", result.id)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
if not shutdown_event.is_set():
|
|
387
|
+
# Create a structured error for better handling
|
|
388
|
+
error_details = {
|
|
389
|
+
"transaction_id": transaction_id,
|
|
390
|
+
"model": model,
|
|
391
|
+
"error_type": type(e).__name__,
|
|
392
|
+
"completion_args_keys": list(completion_args.keys()),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# Log error with structured information
|
|
396
|
+
logger.error(" REVENIUM FAILURE: Error in metering call: %s", str(e))
|
|
397
|
+
logger.error(" REVENIUM FAILURE: Error details: %s", error_details)
|
|
398
|
+
|
|
399
|
+
# Log traceback at debug level to avoid spam
|
|
400
|
+
logger.debug("Metering call traceback:", exc_info=True)
|
|
401
|
+
|
|
402
|
+
# Raise a specific MeteringError for better error handling upstream
|
|
403
|
+
raise MeteringError(
|
|
404
|
+
f"Failed to send metering data: {str(e)}",
|
|
405
|
+
transaction_id=transaction_id,
|
|
406
|
+
api_response=None,
|
|
407
|
+
error_details=error_details,
|
|
408
|
+
) from e
|
|
409
|
+
else:
|
|
410
|
+
logger.debug("Metering call failed during shutdown - this is expected")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def create_metering_call(
|
|
414
|
+
usage_data: UsageData,
|
|
415
|
+
usage_metadata: Dict[str, Any],
|
|
416
|
+
time_to_first_token: int = 0,
|
|
417
|
+
is_streamed: bool = False,
|
|
418
|
+
# Prompt capture fields
|
|
419
|
+
system_prompt: Optional[str] = None,
|
|
420
|
+
input_messages: Optional[str] = None,
|
|
421
|
+
output_response: Optional[str] = None,
|
|
422
|
+
prompts_truncated: Optional[bool] = None,
|
|
423
|
+
) -> None:
|
|
424
|
+
"""
|
|
425
|
+
Create and execute a metering call using UsageData.
|
|
426
|
+
|
|
427
|
+
This is a higher-level function that uses the standardized UsageData structure.
|
|
428
|
+
"""
|
|
429
|
+
# Override streaming and timing info
|
|
430
|
+
usage_data.is_streamed = is_streamed
|
|
431
|
+
usage_data.time_to_first_token = time_to_first_token
|
|
432
|
+
|
|
433
|
+
# Create async metering call
|
|
434
|
+
async def metering_call():
|
|
435
|
+
await log_token_usage(
|
|
436
|
+
transaction_id=usage_data.transaction_id,
|
|
437
|
+
model=usage_data.model,
|
|
438
|
+
prompt_tokens=usage_data.input_token_count, # These are positional parameters
|
|
439
|
+
completion_tokens=usage_data.output_token_count, # These are positional parameters
|
|
440
|
+
total_tokens=usage_data.total_token_count, # These are positional parameters
|
|
441
|
+
cached_tokens=usage_data.cache_creation_token_count,
|
|
442
|
+
stop_reason=usage_data.stop_reason,
|
|
443
|
+
request_time=usage_data.request_time,
|
|
444
|
+
response_time=usage_data.response_time,
|
|
445
|
+
request_duration=usage_data.request_duration,
|
|
446
|
+
usage_metadata=usage_metadata,
|
|
447
|
+
provider=usage_data.provider,
|
|
448
|
+
model_source=usage_data.model_source,
|
|
449
|
+
is_streamed=usage_data.is_streamed,
|
|
450
|
+
time_to_first_token=usage_data.time_to_first_token,
|
|
451
|
+
operation_type=OperationType(usage_data.operation_type),
|
|
452
|
+
# Prompt capture fields
|
|
453
|
+
system_prompt=system_prompt,
|
|
454
|
+
input_messages=input_messages,
|
|
455
|
+
output_response=output_response,
|
|
456
|
+
prompts_truncated=prompts_truncated,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Execute in background thread
|
|
460
|
+
run_async_in_thread(metering_call())
|
|
461
|
+
|
|
462
|
+
# Print usage summary if enabled (fire-and-forget)
|
|
463
|
+
# Get the API key from the client
|
|
464
|
+
revenium_api_key = getattr(client, "api_key", None)
|
|
465
|
+
if revenium_api_key:
|
|
466
|
+
# Support both snake_case and camelCase for trace_id
|
|
467
|
+
trace_id = usage_metadata.get("trace_id")
|
|
468
|
+
if trace_id is None:
|
|
469
|
+
trace_id = usage_metadata.get("traceId")
|
|
470
|
+
|
|
471
|
+
# Run summary printing in background thread to avoid blocking
|
|
472
|
+
def _print_summary_sync():
|
|
473
|
+
print_usage_summary(
|
|
474
|
+
model=usage_data.model,
|
|
475
|
+
provider=usage_data.provider,
|
|
476
|
+
request_duration=usage_data.request_duration,
|
|
477
|
+
input_token_count=usage_data.input_token_count,
|
|
478
|
+
output_token_count=usage_data.output_token_count,
|
|
479
|
+
total_token_count=usage_data.total_token_count,
|
|
480
|
+
transaction_id=usage_data.transaction_id,
|
|
481
|
+
trace_id=trace_id,
|
|
482
|
+
revenium_api_key=revenium_api_key,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
run_async_in_thread(_print_summary_sync)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _build_common_metadata_args(usage_metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
489
|
+
"""
|
|
490
|
+
Build common metadata arguments shared across all metering endpoints.
|
|
491
|
+
|
|
492
|
+
Extracts trace fields, subscriber data, organization/product names, etc.
|
|
493
|
+
from usage_metadata and environment variables.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Dictionary of common keyword arguments for metering API calls.
|
|
497
|
+
"""
|
|
498
|
+
args = {}
|
|
499
|
+
|
|
500
|
+
# Optional metadata fields
|
|
501
|
+
if usage_metadata.get("trace_id"):
|
|
502
|
+
args["trace_id"] = usage_metadata["trace_id"]
|
|
503
|
+
if usage_metadata.get("task_type"):
|
|
504
|
+
args["task_type"] = usage_metadata["task_type"]
|
|
505
|
+
|
|
506
|
+
# Organization and product names with deprecation fallback
|
|
507
|
+
organization_name = extract_field_with_fallback(
|
|
508
|
+
usage_metadata, "organization_name", "organizationName",
|
|
509
|
+
"organization_id", "organizationId", "organization",
|
|
510
|
+
)
|
|
511
|
+
product_name = extract_field_with_fallback(
|
|
512
|
+
usage_metadata, "product_name", "productName",
|
|
513
|
+
"product_id", "productId", "product",
|
|
514
|
+
)
|
|
515
|
+
if organization_name:
|
|
516
|
+
args["organization_name"] = organization_name
|
|
517
|
+
if product_name:
|
|
518
|
+
args["product_name"] = product_name
|
|
519
|
+
|
|
520
|
+
if usage_metadata.get("subscription_id"):
|
|
521
|
+
args["subscription_id"] = usage_metadata["subscription_id"]
|
|
522
|
+
if usage_metadata.get("agent"):
|
|
523
|
+
args["agent"] = usage_metadata["agent"]
|
|
524
|
+
|
|
525
|
+
# Trace visualization fields
|
|
526
|
+
environment = usage_metadata.get("environment") or trace_fields.get_environment()
|
|
527
|
+
if environment:
|
|
528
|
+
args["environment"] = environment
|
|
529
|
+
|
|
530
|
+
region = usage_metadata.get("region") or trace_fields.get_region()
|
|
531
|
+
if region:
|
|
532
|
+
args["region"] = region
|
|
533
|
+
|
|
534
|
+
credential_alias = (
|
|
535
|
+
usage_metadata.get("credential_alias") or
|
|
536
|
+
usage_metadata.get("credentialAlias") or
|
|
537
|
+
trace_fields.get_credential_alias()
|
|
538
|
+
)
|
|
539
|
+
if credential_alias:
|
|
540
|
+
args["credential_alias"] = credential_alias
|
|
541
|
+
|
|
542
|
+
trace_type = (
|
|
543
|
+
usage_metadata.get("trace_type") or
|
|
544
|
+
usage_metadata.get("traceType") or
|
|
545
|
+
trace_fields.get_trace_type()
|
|
546
|
+
)
|
|
547
|
+
if trace_type:
|
|
548
|
+
if usage_metadata.get("trace_type") or usage_metadata.get("traceType"):
|
|
549
|
+
trace_type = trace_fields.validate_trace_type(trace_type)
|
|
550
|
+
if trace_type:
|
|
551
|
+
args["trace_type"] = trace_type
|
|
552
|
+
|
|
553
|
+
trace_name = (
|
|
554
|
+
usage_metadata.get("trace_name") or
|
|
555
|
+
usage_metadata.get("traceName") or
|
|
556
|
+
trace_fields.get_trace_name()
|
|
557
|
+
)
|
|
558
|
+
if trace_name:
|
|
559
|
+
if usage_metadata.get("trace_name") or usage_metadata.get("traceName"):
|
|
560
|
+
trace_name = trace_fields.validate_trace_name(trace_name)
|
|
561
|
+
if trace_name:
|
|
562
|
+
args["trace_name"] = trace_name
|
|
563
|
+
|
|
564
|
+
parent_transaction_id = (
|
|
565
|
+
usage_metadata.get("parent_transaction_id") or
|
|
566
|
+
usage_metadata.get("parentTransactionId") or
|
|
567
|
+
trace_fields.get_parent_transaction_id()
|
|
568
|
+
)
|
|
569
|
+
if parent_transaction_id:
|
|
570
|
+
args["parent_transaction_id"] = parent_transaction_id
|
|
571
|
+
|
|
572
|
+
transaction_name = trace_fields.get_transaction_name(usage_metadata)
|
|
573
|
+
if transaction_name:
|
|
574
|
+
args["transaction_name"] = transaction_name
|
|
575
|
+
|
|
576
|
+
retry_number = trace_fields.get_retry_number()
|
|
577
|
+
if retry_number > 0:
|
|
578
|
+
args["retry_number"] = retry_number
|
|
579
|
+
|
|
580
|
+
# Subscriber data
|
|
581
|
+
subscriber_data = {}
|
|
582
|
+
flat_keys_used = []
|
|
583
|
+
if "subscriber" in usage_metadata:
|
|
584
|
+
nested_subscriber = usage_metadata["subscriber"]
|
|
585
|
+
if isinstance(nested_subscriber, dict):
|
|
586
|
+
subscriber_data = nested_subscriber.copy()
|
|
587
|
+
else:
|
|
588
|
+
subscriber_id = usage_metadata.get("subscriber_id")
|
|
589
|
+
subscriber_email = usage_metadata.get("subscriber_email")
|
|
590
|
+
credential_name = usage_metadata.get("subscriber_credential_name")
|
|
591
|
+
credential_value = usage_metadata.get("subscriber_credential")
|
|
592
|
+
if subscriber_id:
|
|
593
|
+
subscriber_data["id"] = subscriber_id
|
|
594
|
+
flat_keys_used.append("subscriber_id")
|
|
595
|
+
if subscriber_email:
|
|
596
|
+
subscriber_data["email"] = subscriber_email
|
|
597
|
+
flat_keys_used.append("subscriber_email")
|
|
598
|
+
credential_data = {}
|
|
599
|
+
if credential_name:
|
|
600
|
+
credential_data["name"] = credential_name
|
|
601
|
+
flat_keys_used.append("subscriber_credential_name")
|
|
602
|
+
if credential_value:
|
|
603
|
+
credential_data["value"] = credential_value
|
|
604
|
+
flat_keys_used.append("subscriber_credential")
|
|
605
|
+
if credential_data:
|
|
606
|
+
subscriber_data["credential"] = credential_data
|
|
607
|
+
if flat_keys_used:
|
|
608
|
+
logger.warning(
|
|
609
|
+
f"Flat subscriber keys are deprecated: {flat_keys_used}. "
|
|
610
|
+
"Please use nested 'subscriber' object format."
|
|
611
|
+
)
|
|
612
|
+
if subscriber_data:
|
|
613
|
+
args["subscriber"] = subscriber_data
|
|
614
|
+
|
|
615
|
+
return args
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
async def log_image_usage(
|
|
619
|
+
transaction_id: str,
|
|
620
|
+
model: str,
|
|
621
|
+
requested_image_count: int,
|
|
622
|
+
actual_image_count: int,
|
|
623
|
+
request_time: str,
|
|
624
|
+
response_time: str,
|
|
625
|
+
request_duration: int,
|
|
626
|
+
usage_metadata: Dict[str, Any],
|
|
627
|
+
provider: str = "Google",
|
|
628
|
+
model_source: str = "GOOGLE",
|
|
629
|
+
operation_subtype: str = "generation",
|
|
630
|
+
resolution: Optional[str] = None,
|
|
631
|
+
quality: Optional[str] = None,
|
|
632
|
+
style: Optional[str] = None,
|
|
633
|
+
aspect_ratio: Optional[str] = None,
|
|
634
|
+
) -> None:
|
|
635
|
+
"""
|
|
636
|
+
Log image generation usage to Revenium via /meter/v2/ai/images.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
transaction_id: Unique identifier for this API call
|
|
640
|
+
model: Model name (e.g., "imagen-3.0-generate-001")
|
|
641
|
+
requested_image_count: Number of images requested
|
|
642
|
+
actual_image_count: Number of images actually generated
|
|
643
|
+
request_time: ISO timestamp of request start
|
|
644
|
+
response_time: ISO timestamp of response completion
|
|
645
|
+
request_duration: Duration in milliseconds
|
|
646
|
+
usage_metadata: Additional metadata for the request
|
|
647
|
+
provider: Provider name (always "Google")
|
|
648
|
+
model_source: Model source (always "GOOGLE")
|
|
649
|
+
operation_subtype: "generation", "edit", or "upscale"
|
|
650
|
+
resolution: Image resolution (e.g., "1024x1024")
|
|
651
|
+
quality: Image quality setting
|
|
652
|
+
style: Image style setting
|
|
653
|
+
aspect_ratio: Aspect ratio (e.g., "16:9")
|
|
654
|
+
"""
|
|
655
|
+
if shutdown_event.is_set():
|
|
656
|
+
logger.warning("Skipping image metering call during shutdown")
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
logger.debug(
|
|
660
|
+
"Image metering call to Revenium for %s operation %s",
|
|
661
|
+
operation_subtype,
|
|
662
|
+
transaction_id,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
image_args = {
|
|
666
|
+
"model": model,
|
|
667
|
+
"provider": provider,
|
|
668
|
+
"model_source": model_source,
|
|
669
|
+
"request_time": request_time,
|
|
670
|
+
"response_time": response_time,
|
|
671
|
+
"request_duration": int(request_duration),
|
|
672
|
+
"transaction_id": transaction_id,
|
|
673
|
+
"requested_image_count": requested_image_count,
|
|
674
|
+
"actual_image_count": actual_image_count,
|
|
675
|
+
"operation_subtype": operation_subtype,
|
|
676
|
+
"middleware_source": "python",
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if resolution:
|
|
680
|
+
image_args["resolution"] = resolution
|
|
681
|
+
if quality:
|
|
682
|
+
image_args["quality"] = quality
|
|
683
|
+
if style:
|
|
684
|
+
image_args["style"] = style
|
|
685
|
+
if aspect_ratio:
|
|
686
|
+
image_args["aspect_ratio"] = aspect_ratio
|
|
687
|
+
|
|
688
|
+
# Add common metadata
|
|
689
|
+
image_args.update(_build_common_metadata_args(usage_metadata))
|
|
690
|
+
|
|
691
|
+
logger.debug("Calling client.ai.create_image with args: %s", image_args)
|
|
692
|
+
|
|
693
|
+
try:
|
|
694
|
+
result = client.ai.create_image(**image_args)
|
|
695
|
+
logger.debug("Image metering call result: %s", result)
|
|
696
|
+
logger.info(" REVENIUM SUCCESS: Image metering call successful: %s", result.id)
|
|
697
|
+
except Exception as e:
|
|
698
|
+
if not shutdown_event.is_set():
|
|
699
|
+
error_details = {
|
|
700
|
+
"transaction_id": transaction_id,
|
|
701
|
+
"model": model,
|
|
702
|
+
"error_type": type(e).__name__,
|
|
703
|
+
}
|
|
704
|
+
logger.error(" REVENIUM FAILURE: Error in image metering call: %s", str(e))
|
|
705
|
+
logger.error(" REVENIUM FAILURE: Error details: %s", error_details)
|
|
706
|
+
logger.debug("Image metering call traceback:", exc_info=True)
|
|
707
|
+
raise MeteringError(
|
|
708
|
+
f"Failed to send image metering data: {str(e)}",
|
|
709
|
+
transaction_id=transaction_id,
|
|
710
|
+
api_response=None,
|
|
711
|
+
error_details=error_details,
|
|
712
|
+
) from e
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
async def log_video_usage(
|
|
716
|
+
transaction_id: str,
|
|
717
|
+
model: str,
|
|
718
|
+
duration_seconds: float,
|
|
719
|
+
request_time: str,
|
|
720
|
+
response_time: str,
|
|
721
|
+
request_duration: int,
|
|
722
|
+
usage_metadata: Dict[str, Any],
|
|
723
|
+
provider: str = "Google",
|
|
724
|
+
model_source: str = "GOOGLE",
|
|
725
|
+
operation_subtype: str = "generation",
|
|
726
|
+
resolution: Optional[str] = None,
|
|
727
|
+
fps: Optional[int] = None,
|
|
728
|
+
aspect_ratio: Optional[str] = None,
|
|
729
|
+
video_job_id: Optional[str] = None,
|
|
730
|
+
async_operation: bool = False,
|
|
731
|
+
) -> None:
|
|
732
|
+
"""
|
|
733
|
+
Log video generation usage to Revenium via /meter/v2/ai/video.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
transaction_id: Unique identifier for this API call
|
|
737
|
+
model: Model name (e.g., "veo-2.0-generate-001")
|
|
738
|
+
duration_seconds: Duration of generated video in seconds
|
|
739
|
+
request_time: ISO timestamp of request start
|
|
740
|
+
response_time: ISO timestamp of response completion
|
|
741
|
+
request_duration: Duration in milliseconds
|
|
742
|
+
usage_metadata: Additional metadata for the request
|
|
743
|
+
provider: Provider name (always "Google")
|
|
744
|
+
model_source: Model source (always "GOOGLE")
|
|
745
|
+
operation_subtype: "generation", "extend", or "upscale"
|
|
746
|
+
resolution: Video resolution (e.g., "1080p")
|
|
747
|
+
fps: Frames per second
|
|
748
|
+
aspect_ratio: Aspect ratio (e.g., "16:9")
|
|
749
|
+
video_job_id: Job ID for async operations
|
|
750
|
+
async_operation: Whether this was an async operation
|
|
751
|
+
"""
|
|
752
|
+
if shutdown_event.is_set():
|
|
753
|
+
logger.warning("Skipping video metering call during shutdown")
|
|
754
|
+
return
|
|
755
|
+
|
|
756
|
+
logger.debug(
|
|
757
|
+
"Video metering call to Revenium for %s operation %s",
|
|
758
|
+
operation_subtype,
|
|
759
|
+
transaction_id,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
video_args = {
|
|
763
|
+
"model": model,
|
|
764
|
+
"provider": provider,
|
|
765
|
+
"model_source": model_source,
|
|
766
|
+
"request_time": request_time,
|
|
767
|
+
"response_time": response_time,
|
|
768
|
+
"request_duration": int(request_duration),
|
|
769
|
+
"transaction_id": transaction_id,
|
|
770
|
+
"duration_seconds": duration_seconds,
|
|
771
|
+
"operation_subtype": operation_subtype,
|
|
772
|
+
"middleware_source": "python",
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if resolution:
|
|
776
|
+
video_args["resolution"] = resolution
|
|
777
|
+
if fps is not None:
|
|
778
|
+
video_args["fps"] = fps
|
|
779
|
+
if aspect_ratio:
|
|
780
|
+
video_args["aspect_ratio"] = aspect_ratio
|
|
781
|
+
if video_job_id:
|
|
782
|
+
video_args["video_job_id"] = video_job_id
|
|
783
|
+
if async_operation:
|
|
784
|
+
video_args["async_operation"] = async_operation
|
|
785
|
+
|
|
786
|
+
# Add common metadata
|
|
787
|
+
video_args.update(_build_common_metadata_args(usage_metadata))
|
|
788
|
+
|
|
789
|
+
logger.debug("Calling client.ai.create_video with args: %s", video_args)
|
|
790
|
+
|
|
791
|
+
try:
|
|
792
|
+
result = client.ai.create_video(**video_args)
|
|
793
|
+
logger.debug("Video metering call result: %s", result)
|
|
794
|
+
logger.info(" REVENIUM SUCCESS: Video metering call successful: %s", result.id)
|
|
795
|
+
except Exception as e:
|
|
796
|
+
if not shutdown_event.is_set():
|
|
797
|
+
error_details = {
|
|
798
|
+
"transaction_id": transaction_id,
|
|
799
|
+
"model": model,
|
|
800
|
+
"error_type": type(e).__name__,
|
|
801
|
+
}
|
|
802
|
+
logger.error(" REVENIUM FAILURE: Error in video metering call: %s", str(e))
|
|
803
|
+
logger.error(" REVENIUM FAILURE: Error details: %s", error_details)
|
|
804
|
+
logger.debug("Video metering call traceback:", exc_info=True)
|
|
805
|
+
raise MeteringError(
|
|
806
|
+
f"Failed to send video metering data: {str(e)}",
|
|
807
|
+
transaction_id=transaction_id,
|
|
808
|
+
api_response=None,
|
|
809
|
+
error_details=error_details,
|
|
810
|
+
) from e
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def create_image_metering_call(
|
|
814
|
+
model: str,
|
|
815
|
+
requested_image_count: int,
|
|
816
|
+
actual_image_count: int,
|
|
817
|
+
request_time_dt: datetime.datetime,
|
|
818
|
+
response_time_dt: datetime.datetime,
|
|
819
|
+
usage_metadata: Dict[str, Any],
|
|
820
|
+
provider: str = "Google",
|
|
821
|
+
model_source: str = "GOOGLE",
|
|
822
|
+
operation_subtype: str = "generation",
|
|
823
|
+
resolution: Optional[str] = None,
|
|
824
|
+
quality: Optional[str] = None,
|
|
825
|
+
style: Optional[str] = None,
|
|
826
|
+
aspect_ratio: Optional[str] = None,
|
|
827
|
+
) -> None:
|
|
828
|
+
"""Create and execute an image metering call in a background thread."""
|
|
829
|
+
transaction_id = generate_transaction_id()
|
|
830
|
+
request_time = format_timestamp(request_time_dt)
|
|
831
|
+
response_time = format_timestamp(response_time_dt)
|
|
832
|
+
request_duration = calculate_duration_ms(request_time_dt, response_time_dt)
|
|
833
|
+
|
|
834
|
+
async def metering_call():
|
|
835
|
+
await log_image_usage(
|
|
836
|
+
transaction_id=transaction_id,
|
|
837
|
+
model=model,
|
|
838
|
+
requested_image_count=requested_image_count,
|
|
839
|
+
actual_image_count=actual_image_count,
|
|
840
|
+
request_time=request_time,
|
|
841
|
+
response_time=response_time,
|
|
842
|
+
request_duration=request_duration,
|
|
843
|
+
usage_metadata=usage_metadata,
|
|
844
|
+
provider=provider,
|
|
845
|
+
model_source=model_source,
|
|
846
|
+
operation_subtype=operation_subtype,
|
|
847
|
+
resolution=resolution,
|
|
848
|
+
quality=quality,
|
|
849
|
+
style=style,
|
|
850
|
+
aspect_ratio=aspect_ratio,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
run_async_in_thread(metering_call())
|
|
854
|
+
|
|
855
|
+
# Print usage summary if enabled
|
|
856
|
+
revenium_api_key = getattr(client, "api_key", None)
|
|
857
|
+
if revenium_api_key:
|
|
858
|
+
trace_id = usage_metadata.get("trace_id") or usage_metadata.get("traceId")
|
|
859
|
+
|
|
860
|
+
def _print_summary_sync():
|
|
861
|
+
print_usage_summary(
|
|
862
|
+
model=model,
|
|
863
|
+
provider=provider,
|
|
864
|
+
request_duration=request_duration,
|
|
865
|
+
input_token_count=0,
|
|
866
|
+
output_token_count=0,
|
|
867
|
+
total_token_count=0,
|
|
868
|
+
transaction_id=transaction_id,
|
|
869
|
+
trace_id=trace_id,
|
|
870
|
+
revenium_api_key=revenium_api_key,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
run_async_in_thread(_print_summary_sync)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def create_video_metering_call(
|
|
877
|
+
model: str,
|
|
878
|
+
duration_seconds: float,
|
|
879
|
+
request_time_dt: datetime.datetime,
|
|
880
|
+
response_time_dt: datetime.datetime,
|
|
881
|
+
usage_metadata: Dict[str, Any],
|
|
882
|
+
provider: str = "Google",
|
|
883
|
+
model_source: str = "GOOGLE",
|
|
884
|
+
operation_subtype: str = "generation",
|
|
885
|
+
resolution: Optional[str] = None,
|
|
886
|
+
fps: Optional[int] = None,
|
|
887
|
+
aspect_ratio: Optional[str] = None,
|
|
888
|
+
video_job_id: Optional[str] = None,
|
|
889
|
+
async_operation: bool = False,
|
|
890
|
+
) -> None:
|
|
891
|
+
"""Create and execute a video metering call in a background thread."""
|
|
892
|
+
transaction_id = generate_transaction_id()
|
|
893
|
+
request_time = format_timestamp(request_time_dt)
|
|
894
|
+
response_time = format_timestamp(response_time_dt)
|
|
895
|
+
request_duration = calculate_duration_ms(request_time_dt, response_time_dt)
|
|
896
|
+
|
|
897
|
+
async def metering_call():
|
|
898
|
+
await log_video_usage(
|
|
899
|
+
transaction_id=transaction_id,
|
|
900
|
+
model=model,
|
|
901
|
+
duration_seconds=duration_seconds,
|
|
902
|
+
request_time=request_time,
|
|
903
|
+
response_time=response_time,
|
|
904
|
+
request_duration=request_duration,
|
|
905
|
+
usage_metadata=usage_metadata,
|
|
906
|
+
provider=provider,
|
|
907
|
+
model_source=model_source,
|
|
908
|
+
operation_subtype=operation_subtype,
|
|
909
|
+
resolution=resolution,
|
|
910
|
+
fps=fps,
|
|
911
|
+
aspect_ratio=aspect_ratio,
|
|
912
|
+
video_job_id=video_job_id,
|
|
913
|
+
async_operation=async_operation,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
run_async_in_thread(metering_call())
|
|
917
|
+
|
|
918
|
+
# Print usage summary if enabled
|
|
919
|
+
revenium_api_key = getattr(client, "api_key", None)
|
|
920
|
+
if revenium_api_key:
|
|
921
|
+
trace_id = usage_metadata.get("trace_id") or usage_metadata.get("traceId")
|
|
922
|
+
|
|
923
|
+
def _print_summary_sync():
|
|
924
|
+
print_usage_summary(
|
|
925
|
+
model=model,
|
|
926
|
+
provider=provider,
|
|
927
|
+
request_duration=request_duration,
|
|
928
|
+
input_token_count=0,
|
|
929
|
+
output_token_count=0,
|
|
930
|
+
total_token_count=0,
|
|
931
|
+
transaction_id=transaction_id,
|
|
932
|
+
trace_id=trace_id,
|
|
933
|
+
revenium_api_key=revenium_api_key,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
run_async_in_thread(_print_summary_sync)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@safe_extract
|
|
940
|
+
def extract_model_name(response: Any, fallback: Optional[str] = None) -> str:
|
|
941
|
+
"""
|
|
942
|
+
Extract model name from API response with fallback.
|
|
943
|
+
|
|
944
|
+
Args:
|
|
945
|
+
response: API response object
|
|
946
|
+
fallback: Fallback model name if extraction fails
|
|
947
|
+
|
|
948
|
+
Returns:
|
|
949
|
+
Model name string
|
|
950
|
+
|
|
951
|
+
Raises:
|
|
952
|
+
TokenExtractionError: If extraction fails and no fallback is provided
|
|
953
|
+
"""
|
|
954
|
+
if response is None:
|
|
955
|
+
if fallback:
|
|
956
|
+
logger.debug("Response is None, using fallback model name: %s", fallback)
|
|
957
|
+
return fallback
|
|
958
|
+
raise APIResponseError("Response is None and no fallback model name provided")
|
|
959
|
+
|
|
960
|
+
# Try common model name attributes using safe access
|
|
961
|
+
model_attrs = ["model", "model_name", "_model_name", "model_version"]
|
|
962
|
+
for attr in model_attrs:
|
|
963
|
+
model_name = safe_getattr(response, attr)
|
|
964
|
+
if model_name:
|
|
965
|
+
return str(model_name)
|
|
966
|
+
|
|
967
|
+
# Try nested model attributes
|
|
968
|
+
usage = safe_getattr(response, "usage")
|
|
969
|
+
if usage:
|
|
970
|
+
model_name = safe_getattr(usage, "model")
|
|
971
|
+
if model_name:
|
|
972
|
+
return str(model_name)
|
|
973
|
+
|
|
974
|
+
# Use fallback if provided
|
|
975
|
+
if fallback:
|
|
976
|
+
logger.debug(
|
|
977
|
+
"Could not extract model name from response, using fallback: %s", fallback
|
|
978
|
+
)
|
|
979
|
+
return fallback
|
|
980
|
+
|
|
981
|
+
# Log available attributes for debugging
|
|
982
|
+
available_attrs = [attr for attr in dir(response) if not attr.startswith("_")]
|
|
983
|
+
logger.warning(
|
|
984
|
+
"Could not extract model name from response. Available attributes: %s",
|
|
985
|
+
available_attrs[:10],
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
return "unknown-model"
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@safe_extract
|
|
992
|
+
def extract_token_counts(response: Any, operation_type: OperationType) -> TokenCounts:
|
|
993
|
+
"""
|
|
994
|
+
Extract token counts from API response.
|
|
995
|
+
|
|
996
|
+
This function handles the differences between SDKs and operation types.
|
|
997
|
+
|
|
998
|
+
Args:
|
|
999
|
+
response: API response object
|
|
1000
|
+
operation_type: Type of operation (CHAT or EMBED)
|
|
1001
|
+
|
|
1002
|
+
Returns:
|
|
1003
|
+
TokenCounts object with extracted counts
|
|
1004
|
+
|
|
1005
|
+
Raises:
|
|
1006
|
+
TokenExtractionError: If response is invalid
|
|
1007
|
+
"""
|
|
1008
|
+
if response is None:
|
|
1009
|
+
logger.warning("Response is None, returning zero token counts")
|
|
1010
|
+
return TokenCounts(
|
|
1011
|
+
input_tokens=0, output_tokens=0, total_tokens=0, cached_tokens=0
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# Initialize with zeros
|
|
1015
|
+
input_tokens = 0
|
|
1016
|
+
output_tokens = 0
|
|
1017
|
+
total_tokens = 0
|
|
1018
|
+
cached_tokens = 0
|
|
1019
|
+
|
|
1020
|
+
# Try to extract from usage_metadata first (Google AI SDK pattern)
|
|
1021
|
+
usage = safe_getattr(response, "usage_metadata") or safe_getattr(response, "usage")
|
|
1022
|
+
|
|
1023
|
+
if usage and has_token_counts(usage):
|
|
1024
|
+
# Use the utility function for safe token extraction
|
|
1025
|
+
input_tokens = get_token_count(
|
|
1026
|
+
usage, ["prompt_token_count", "prompt_tokens", "input_tokens"]
|
|
1027
|
+
)
|
|
1028
|
+
output_tokens = get_token_count(
|
|
1029
|
+
usage, ["candidates_token_count", "completion_tokens", "output_tokens"]
|
|
1030
|
+
)
|
|
1031
|
+
total_tokens = get_token_count(usage, ["total_token_count", "total_tokens"])
|
|
1032
|
+
cached_tokens = get_token_count(
|
|
1033
|
+
usage, ["cached_content_token_count", "cached_tokens"]
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
logger.debug(
|
|
1037
|
+
"Extracted token counts from usage: input=%d, output=%d, total=%d, cached=%d",
|
|
1038
|
+
input_tokens,
|
|
1039
|
+
output_tokens,
|
|
1040
|
+
total_tokens,
|
|
1041
|
+
cached_tokens,
|
|
1042
|
+
)
|
|
1043
|
+
else:
|
|
1044
|
+
logger.debug("No usage metadata found or no token counts available in response")
|
|
1045
|
+
|
|
1046
|
+
# For embeddings, output tokens are always 0
|
|
1047
|
+
if operation_type == OperationType.EMBED:
|
|
1048
|
+
output_tokens = 0
|
|
1049
|
+
logger.debug("Operation type is EMBED, setting output_tokens to 0")
|
|
1050
|
+
|
|
1051
|
+
# Calculate total if not provided but we have input/output
|
|
1052
|
+
if total_tokens == 0 and (input_tokens > 0 or output_tokens > 0):
|
|
1053
|
+
total_tokens = input_tokens + output_tokens
|
|
1054
|
+
logger.debug(
|
|
1055
|
+
"Calculated total_tokens as %d (input=%d + output=%d)",
|
|
1056
|
+
total_tokens,
|
|
1057
|
+
input_tokens,
|
|
1058
|
+
output_tokens,
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
return TokenCounts(
|
|
1062
|
+
input_tokens=input_tokens,
|
|
1063
|
+
output_tokens=output_tokens,
|
|
1064
|
+
total_tokens=total_tokens,
|
|
1065
|
+
cached_tokens=cached_tokens,
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def create_usage_data(
|
|
1070
|
+
response: Any,
|
|
1071
|
+
operation_type: OperationType,
|
|
1072
|
+
provider_metadata: ProviderMetadata,
|
|
1073
|
+
request_time: datetime.datetime,
|
|
1074
|
+
response_time: datetime.datetime,
|
|
1075
|
+
model_name_fallback: Optional[str] = None,
|
|
1076
|
+
stop_reason_fallback: str = "END",
|
|
1077
|
+
) -> UsageData:
|
|
1078
|
+
"""
|
|
1079
|
+
Create standardized UsageData from API response.
|
|
1080
|
+
|
|
1081
|
+
This is the main function for converting SDK-specific responses
|
|
1082
|
+
to our common UsageData format.
|
|
1083
|
+
"""
|
|
1084
|
+
# Extract token counts
|
|
1085
|
+
token_counts = extract_token_counts(response, operation_type)
|
|
1086
|
+
|
|
1087
|
+
# Extract model name
|
|
1088
|
+
model_name = extract_model_name(response, model_name_fallback)
|
|
1089
|
+
|
|
1090
|
+
# Extract stop reason (SDK-specific logic should be handled by caller)
|
|
1091
|
+
stop_reason = stop_reason_fallback
|
|
1092
|
+
if hasattr(response, "finish_reason"):
|
|
1093
|
+
stop_reason = response.finish_reason or stop_reason_fallback
|
|
1094
|
+
elif hasattr(response, "candidates") and response.candidates:
|
|
1095
|
+
candidate = response.candidates[0]
|
|
1096
|
+
if hasattr(candidate, "finish_reason"):
|
|
1097
|
+
stop_reason = candidate.finish_reason or stop_reason_fallback
|
|
1098
|
+
|
|
1099
|
+
# Create UsageData
|
|
1100
|
+
return UsageData.create(
|
|
1101
|
+
operation_type=operation_type,
|
|
1102
|
+
input_tokens=token_counts.input_tokens,
|
|
1103
|
+
output_tokens=token_counts.output_tokens,
|
|
1104
|
+
total_tokens=token_counts.total_tokens,
|
|
1105
|
+
model=model_name,
|
|
1106
|
+
provider_metadata=provider_metadata,
|
|
1107
|
+
stop_reason=stop_reason,
|
|
1108
|
+
request_time=request_time,
|
|
1109
|
+
response_time=response_time,
|
|
1110
|
+
cache_creation_token_count=token_counts.cached_tokens,
|
|
1111
|
+
)
|