revenium-python-sdk 0.1.7__tar.gz → 0.1.8__tar.gz
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_python_sdk-0.1.7/revenium_python_sdk.egg-info → revenium_python_sdk-0.1.8}/PKG-INFO +1 -1
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/pyproject.toml +1 -1
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/bedrock_adapter.py +142 -47
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/middleware.py +9 -6
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/langchain/unified_handler.py +99 -20
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/middleware.py +107 -29
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8/revenium_python_sdk.egg-info}/PKG-INFO +1 -1
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/LICENSE +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/README.md +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/config.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/context.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/decorators.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/enforcement.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/exceptions.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/metering.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/metering_submission.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/patch_registry.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/prompt_extraction.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/subscriber.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/agentic_outcomes.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/config.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/provider.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/_metering.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/config.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/middleware.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/exceptions.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/protocols.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/types.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/utils.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/config.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/google_ai/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/google_ai/middleware.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/google_ai/provider.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/vertex_ai/middleware.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/vertex_ai/provider.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/config.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/context.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/decorators.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/hooks.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/middleware.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/validation.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/proxy/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/proxy/middleware.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/ollama/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/ollama/middleware.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/ollama/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/azure_config.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/azure_model_resolver.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/config.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/exceptions.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/langchain/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/langchain/_utils.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/provider.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/middleware.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/perplexity_sdk.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/provider.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/webhooks/__init__.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/webhooks/_verify.py +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_python_sdk.egg-info/SOURCES.txt +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_python_sdk.egg-info/requires.txt +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_python_sdk.egg-info/top_level.txt +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/setup.cfg +0 -0
- {revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/tests/test_metering.py +0 -0
{revenium_python_sdk-0.1.7/revenium_python_sdk.egg-info → revenium_python_sdk-0.1.8}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: revenium-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: The official Revenium Python SDK — unified AI metering middleware for OpenAI, Anthropic, Google, Ollama, LiteLLM, Perplexity, and fal.ai.
|
|
5
5
|
Author-email: Revenium <support@revenium.io>
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "revenium-python-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
description = "The official Revenium Python SDK — unified AI metering middleware for OpenAI, Anthropic, Google, Ollama, LiteLLM, Perplexity, and fal.ai."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -169,7 +169,74 @@ def _model_id(model_name: str) -> str:
|
|
|
169
169
|
return _MODEL_MAP.get(model_name, f"anthropic.{model_name}")
|
|
170
170
|
|
|
171
171
|
|
|
172
|
-
def
|
|
172
|
+
def _as_dict(value: Any) -> dict:
|
|
173
|
+
"""Return value when it is a dict, otherwise an empty dict."""
|
|
174
|
+
return value if isinstance(value, dict) else {}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _token_count(value: Any) -> Optional[int]:
|
|
178
|
+
"""Return a non-negative integer token count, or None for invalid values."""
|
|
179
|
+
if isinstance(value, bool):
|
|
180
|
+
return None
|
|
181
|
+
if isinstance(value, int):
|
|
182
|
+
return value if value >= 0 else None
|
|
183
|
+
if isinstance(value, str) and value.isdigit():
|
|
184
|
+
return int(value)
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _first_token_count(source: Any, keys: Tuple[str, ...]) -> Optional[int]:
|
|
189
|
+
"""Return the first valid token count for any of the supplied keys."""
|
|
190
|
+
source = _as_dict(source)
|
|
191
|
+
for key in keys:
|
|
192
|
+
if key in source:
|
|
193
|
+
value = _token_count(source.get(key))
|
|
194
|
+
if value is not None:
|
|
195
|
+
return value
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _extract_cache_tokens(usage: Any) -> Tuple[Optional[int], Optional[int]]:
|
|
200
|
+
"""Return (cache_creation_tokens, cache_read_tokens) from a Bedrock usage dict."""
|
|
201
|
+
cache_creation = _first_token_count(
|
|
202
|
+
usage,
|
|
203
|
+
(
|
|
204
|
+
"cacheWriteInputTokensCount",
|
|
205
|
+
"cacheWriteInputTokens",
|
|
206
|
+
"cache_write_input_tokens_count",
|
|
207
|
+
"cache_write_input_tokens",
|
|
208
|
+
"cache_creation_input_tokens_count",
|
|
209
|
+
"cache_creation_input_tokens",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
cache_read = _first_token_count(
|
|
213
|
+
usage,
|
|
214
|
+
(
|
|
215
|
+
"cacheReadInputTokensCount",
|
|
216
|
+
"cacheReadInputTokens",
|
|
217
|
+
"cache_read_input_tokens_count",
|
|
218
|
+
"cache_read_input_tokens",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
return cache_creation, cache_read
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _extract_token_counts(usage: Any, metrics: Optional[Any] = None) -> Tuple[Optional[int], Optional[int]]:
|
|
225
|
+
"""Return (input_tokens, output_tokens) from Bedrock usage and metrics blocks."""
|
|
226
|
+
usage = _as_dict(usage)
|
|
227
|
+
metrics = _as_dict(metrics)
|
|
228
|
+
input_tokens = _first_token_count(usage, ("inputTokens", "input_tokens", "prompt_tokens"))
|
|
229
|
+
if input_tokens is None:
|
|
230
|
+
input_tokens = _first_token_count(metrics, ("inputTokenCount",))
|
|
231
|
+
|
|
232
|
+
output_tokens = _first_token_count(usage, ("outputTokens", "output_tokens", "completion_tokens"))
|
|
233
|
+
if output_tokens is None:
|
|
234
|
+
output_tokens = _first_token_count(metrics, ("outputTokenCount",))
|
|
235
|
+
|
|
236
|
+
return input_tokens, output_tokens
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def bedrock_invoke(model: str, payload: dict, region: Optional[str] = None) -> Tuple[str, int, int, int, int]:
|
|
173
240
|
"""
|
|
174
241
|
Invoke Bedrock model with Anthropic-compatible parameters.
|
|
175
242
|
|
|
@@ -179,7 +246,7 @@ def bedrock_invoke(model: str, payload: dict, region: Optional[str] = None) -> T
|
|
|
179
246
|
region: AWS region (defaults to AWS_REGION env var or us-east-1)
|
|
180
247
|
|
|
181
248
|
Returns:
|
|
182
|
-
Tuple of (text_content, input_tokens, output_tokens)
|
|
249
|
+
Tuple of (text_content, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens)
|
|
183
250
|
|
|
184
251
|
Raises:
|
|
185
252
|
BedrockValidationError: For invalid input parameters
|
|
@@ -225,22 +292,15 @@ def bedrock_invoke(model: str, payload: dict, region: Optional[str] = None) -> T
|
|
|
225
292
|
if c.get("type") == "text"
|
|
226
293
|
)
|
|
227
294
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if
|
|
234
|
-
input_tokens = usage.get("input_tokens", 0)
|
|
235
|
-
output_tokens = usage.get("output_tokens", 0)
|
|
236
|
-
|
|
237
|
-
# Try other possible field names
|
|
238
|
-
if input_tokens == 0 and output_tokens == 0:
|
|
239
|
-
input_tokens = usage.get("prompt_tokens", 0)
|
|
240
|
-
output_tokens = usage.get("completion_tokens", 0)
|
|
295
|
+
input_tokens, output_tokens = _extract_token_counts(usage)
|
|
296
|
+
cache_creation_tokens, cache_read_tokens = _extract_cache_tokens(usage)
|
|
297
|
+
input_tokens = input_tokens if input_tokens is not None else 0
|
|
298
|
+
output_tokens = output_tokens if output_tokens is not None else 0
|
|
299
|
+
cache_creation_tokens = cache_creation_tokens if cache_creation_tokens is not None else 0
|
|
300
|
+
cache_read_tokens = cache_read_tokens if cache_read_tokens is not None else 0
|
|
241
301
|
|
|
242
302
|
logger.debug(f"Bedrock invoke successful: {input_tokens} input tokens, {output_tokens} output tokens")
|
|
243
|
-
return text, input_tokens, output_tokens
|
|
303
|
+
return text, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
|
|
244
304
|
|
|
245
305
|
except BedrockValidationError:
|
|
246
306
|
# Re-raise validation errors as-is
|
|
@@ -267,6 +327,8 @@ class BedrockStreamIterator:
|
|
|
267
327
|
self.accumulated_text = ""
|
|
268
328
|
self.input_tokens = 0
|
|
269
329
|
self.output_tokens = 0
|
|
330
|
+
self.cache_creation_tokens = 0
|
|
331
|
+
self.cache_read_tokens = 0
|
|
270
332
|
self._stream = None
|
|
271
333
|
self._started = False
|
|
272
334
|
|
|
@@ -314,7 +376,17 @@ class BedrockStreamIterator:
|
|
|
314
376
|
def _process_stream(self, stream):
|
|
315
377
|
"""Process the Bedrock stream and yield text chunks."""
|
|
316
378
|
for event in stream:
|
|
317
|
-
|
|
379
|
+
if not hasattr(event, "get"):
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
metadata = _as_dict(event.get("metadata"))
|
|
383
|
+
if metadata:
|
|
384
|
+
self._update_usage_from_blocks(
|
|
385
|
+
metadata.get("usage") or {},
|
|
386
|
+
metadata.get("metrics") or {}
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
chunk = _as_dict(event.get("chunk"))
|
|
318
390
|
if not chunk:
|
|
319
391
|
continue
|
|
320
392
|
|
|
@@ -324,13 +396,25 @@ class BedrockStreamIterator:
|
|
|
324
396
|
|
|
325
397
|
try:
|
|
326
398
|
chunk_data = json.loads(chunk_bytes.decode("utf-8"))
|
|
399
|
+
if not isinstance(chunk_data, dict):
|
|
400
|
+
continue
|
|
401
|
+
|
|
327
402
|
logger.debug(f"Bedrock stream chunk: {chunk_data}")
|
|
328
403
|
|
|
329
404
|
chunk_type = chunk_data.get("type")
|
|
405
|
+
usage = _as_dict(chunk_data.get("usage"))
|
|
406
|
+
metrics = _as_dict(chunk_data.get("amazon-bedrock-invocationMetrics"))
|
|
407
|
+
if usage or metrics:
|
|
408
|
+
self._update_usage_from_blocks(usage, metrics)
|
|
409
|
+
|
|
410
|
+
if chunk_type == "message_start":
|
|
411
|
+
message_usage = _as_dict(_as_dict(chunk_data.get("message")).get("usage"))
|
|
412
|
+
if message_usage:
|
|
413
|
+
self._update_usage_from_blocks(message_usage)
|
|
330
414
|
|
|
331
415
|
# Handle content block delta (text chunk)
|
|
332
416
|
if chunk_type == "content_block_delta":
|
|
333
|
-
delta = chunk_data.get("delta"
|
|
417
|
+
delta = _as_dict(chunk_data.get("delta"))
|
|
334
418
|
text = delta.get("text", "")
|
|
335
419
|
if text:
|
|
336
420
|
self.accumulated_text += text
|
|
@@ -338,27 +422,14 @@ class BedrockStreamIterator:
|
|
|
338
422
|
|
|
339
423
|
# Handle message stop (final message with usage)
|
|
340
424
|
elif chunk_type == "message_stop":
|
|
341
|
-
# Check for usage in the chunk data
|
|
342
|
-
usage = chunk_data.get("usage", {})
|
|
343
|
-
# Also check for Amazon Bedrock invocation metrics
|
|
344
|
-
metrics = chunk_data.get("amazon-bedrock-invocationMetrics", {})
|
|
345
|
-
|
|
346
|
-
# Try multiple token field formats
|
|
347
|
-
self.input_tokens = (
|
|
348
|
-
usage.get("inputTokens", 0) or
|
|
349
|
-
usage.get("input_tokens", 0) or
|
|
350
|
-
metrics.get("inputTokenCount", 0)
|
|
351
|
-
)
|
|
352
|
-
self.output_tokens = (
|
|
353
|
-
usage.get("outputTokens", 0) or
|
|
354
|
-
usage.get("output_tokens", 0) or
|
|
355
|
-
metrics.get("outputTokenCount", 0)
|
|
356
|
-
)
|
|
357
|
-
|
|
358
425
|
logger.debug(f"Bedrock streaming completed. Input tokens: {self.input_tokens}, Output tokens: {self.output_tokens}")
|
|
359
426
|
|
|
427
|
+
# Handle message delta usage-only updates.
|
|
428
|
+
elif chunk_type == "message_delta":
|
|
429
|
+
logger.debug("Bedrock stream: message_delta")
|
|
430
|
+
|
|
360
431
|
# Handle other chunk types (content_block_start, etc.)
|
|
361
|
-
elif chunk_type
|
|
432
|
+
elif chunk_type == "content_block_start":
|
|
362
433
|
logger.debug(f"Bedrock stream: {chunk_type}")
|
|
363
434
|
|
|
364
435
|
except json.JSONDecodeError as e:
|
|
@@ -367,6 +438,22 @@ class BedrockStreamIterator:
|
|
|
367
438
|
|
|
368
439
|
logger.debug(f"Bedrock streaming finished. Total text length: {len(self.accumulated_text)}")
|
|
369
440
|
|
|
441
|
+
def _update_usage_from_blocks(self, usage: Any, metrics: Optional[Any] = None):
|
|
442
|
+
"""Update token counters from a stream usage block without resetting missing fields."""
|
|
443
|
+
usage = _as_dict(usage)
|
|
444
|
+
metrics = _as_dict(metrics)
|
|
445
|
+
input_tokens, output_tokens = _extract_token_counts(usage, metrics)
|
|
446
|
+
if input_tokens is not None:
|
|
447
|
+
self.input_tokens = input_tokens
|
|
448
|
+
if output_tokens is not None:
|
|
449
|
+
self.output_tokens = output_tokens
|
|
450
|
+
|
|
451
|
+
cache_creation_tokens, cache_read_tokens = _extract_cache_tokens(usage)
|
|
452
|
+
if cache_creation_tokens is not None:
|
|
453
|
+
self.cache_creation_tokens = cache_creation_tokens
|
|
454
|
+
if cache_read_tokens is not None:
|
|
455
|
+
self.cache_read_tokens = cache_read_tokens
|
|
456
|
+
|
|
370
457
|
|
|
371
458
|
def bedrock_invoke_stream(model: str, payload: dict, region: Optional[str] = None) -> BedrockStreamIterator:
|
|
372
459
|
"""
|
|
@@ -451,7 +538,8 @@ def create_bedrock_payload(messages: Union[list, List[Dict[str, Any]]], **kwargs
|
|
|
451
538
|
|
|
452
539
|
|
|
453
540
|
def create_anthropic_response(text: str, input_tokens: int, output_tokens: int,
|
|
454
|
-
model: str, request_id: Optional[str] = None
|
|
541
|
+
model: str, request_id: Optional[str] = None,
|
|
542
|
+
cache_creation_tokens: int = 0, cache_read_tokens: int = 0):
|
|
455
543
|
"""
|
|
456
544
|
Create an Anthropic-compatible response object.
|
|
457
545
|
|
|
@@ -461,6 +549,8 @@ def create_anthropic_response(text: str, input_tokens: int, output_tokens: int,
|
|
|
461
549
|
output_tokens: Number of output tokens
|
|
462
550
|
model: Model name
|
|
463
551
|
request_id: Optional request ID
|
|
552
|
+
cache_creation_tokens: Number of cache creation input tokens (default 0)
|
|
553
|
+
cache_read_tokens: Number of cache read input tokens (default 0)
|
|
464
554
|
|
|
465
555
|
Returns:
|
|
466
556
|
Object that mimics Anthropic's Message response structure with both
|
|
@@ -510,26 +600,25 @@ def create_anthropic_response(text: str, input_tokens: int, output_tokens: int,
|
|
|
510
600
|
self.text = text
|
|
511
601
|
|
|
512
602
|
class Usage(HybridAccessMixin):
|
|
513
|
-
def __init__(self, input_tokens, output_tokens):
|
|
603
|
+
def __init__(self, input_tokens, output_tokens, cache_creation_tokens=0, cache_read_tokens=0):
|
|
514
604
|
self.input_tokens = input_tokens
|
|
515
605
|
self.output_tokens = output_tokens
|
|
516
606
|
self.total_tokens = input_tokens + output_tokens
|
|
517
|
-
|
|
518
|
-
self.
|
|
519
|
-
self.cache_read_input_tokens = 0
|
|
607
|
+
self.cache_creation_input_tokens = cache_creation_tokens
|
|
608
|
+
self.cache_read_input_tokens = cache_read_tokens
|
|
520
609
|
|
|
521
610
|
class Message(HybridAccessMixin):
|
|
522
|
-
def __init__(self, text, input_tokens, output_tokens, model, request_id):
|
|
611
|
+
def __init__(self, text, input_tokens, output_tokens, model, request_id, cache_creation_tokens=0, cache_read_tokens=0):
|
|
523
612
|
self.id = request_id or _generate_safe_id("msg_bedrock", text)
|
|
524
613
|
self.type = "message"
|
|
525
614
|
self.role = "assistant"
|
|
526
615
|
self.model = model
|
|
527
616
|
self.content = [TextBlock(text)]
|
|
528
|
-
self.usage = Usage(input_tokens, output_tokens)
|
|
617
|
+
self.usage = Usage(input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens)
|
|
529
618
|
self.stop_reason = "end_turn"
|
|
530
619
|
self.stop_sequence = None
|
|
531
620
|
|
|
532
|
-
return Message(text, input_tokens, output_tokens, model, request_id)
|
|
621
|
+
return Message(text, input_tokens, output_tokens, model, request_id, cache_creation_tokens, cache_read_tokens)
|
|
533
622
|
|
|
534
623
|
|
|
535
624
|
class BedrockStreamWrapper:
|
|
@@ -558,6 +647,8 @@ class BedrockStreamWrapper:
|
|
|
558
647
|
self.first_token_time = None
|
|
559
648
|
self.request_start_time = time.time() * 1000 # Convert to milliseconds
|
|
560
649
|
self.accumulated_text = ""
|
|
650
|
+
self.cache_creation_tokens = 0
|
|
651
|
+
self.cache_read_tokens = 0
|
|
561
652
|
|
|
562
653
|
def __enter__(self):
|
|
563
654
|
"""Enter the context manager and initialize the stream."""
|
|
@@ -635,6 +726,8 @@ class BedrockStreamWrapper:
|
|
|
635
726
|
# Get token counts from the stream iterator
|
|
636
727
|
input_tokens = getattr(self.stream_iterator, 'input_tokens', 0)
|
|
637
728
|
output_tokens = getattr(self.stream_iterator, 'output_tokens', 0)
|
|
729
|
+
self.cache_creation_tokens = getattr(self.stream_iterator, 'cache_creation_tokens', 0)
|
|
730
|
+
self.cache_read_tokens = getattr(self.stream_iterator, 'cache_read_tokens', 0)
|
|
638
731
|
|
|
639
732
|
# Generate a response ID
|
|
640
733
|
self.response_id = _generate_safe_id("msg_bedrock_stream", self.accumulated_text)
|
|
@@ -645,7 +738,9 @@ class BedrockStreamWrapper:
|
|
|
645
738
|
input_tokens=input_tokens,
|
|
646
739
|
output_tokens=output_tokens,
|
|
647
740
|
model=self.model,
|
|
648
|
-
request_id=self.response_id
|
|
741
|
+
request_id=self.response_id,
|
|
742
|
+
cache_creation_tokens=self.cache_creation_tokens,
|
|
743
|
+
cache_read_tokens=self.cache_read_tokens
|
|
649
744
|
)
|
|
650
745
|
|
|
651
746
|
def _send_metering_data(self, request_duration: float):
|
|
@@ -707,8 +802,8 @@ class BedrockStreamWrapper:
|
|
|
707
802
|
meta = extract_common_metadata(self.usage_metadata)
|
|
708
803
|
|
|
709
804
|
result = submit_ai_event("completion", {
|
|
710
|
-
"cache_creation_token_count":
|
|
711
|
-
"cache_read_token_count":
|
|
805
|
+
"cache_creation_token_count": self.cache_creation_tokens,
|
|
806
|
+
"cache_read_token_count": self.cache_read_tokens,
|
|
712
807
|
"input_token_cost": None,
|
|
713
808
|
"output_token_cost": None,
|
|
714
809
|
"total_cost": None,
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/middleware.py
RENAMED
|
@@ -165,14 +165,16 @@ def _handle_bedrock_request(args, kwargs, usage_metadata, request_time_dt, reque
|
|
|
165
165
|
payload = create_bedrock_payload(messages, **bedrock_kwargs)
|
|
166
166
|
|
|
167
167
|
# Invoke Bedrock
|
|
168
|
-
text, input_tokens, output_tokens = bedrock_invoke(model, payload)
|
|
168
|
+
text, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens = bedrock_invoke(model, payload)
|
|
169
169
|
|
|
170
170
|
# Create Anthropic-compatible response
|
|
171
171
|
response = create_anthropic_response(
|
|
172
172
|
text=text,
|
|
173
173
|
input_tokens=input_tokens,
|
|
174
174
|
output_tokens=output_tokens,
|
|
175
|
-
model=model
|
|
175
|
+
model=model,
|
|
176
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
177
|
+
cache_read_tokens=cache_read_tokens
|
|
176
178
|
)
|
|
177
179
|
|
|
178
180
|
# Calculate timing
|
|
@@ -182,7 +184,8 @@ def _handle_bedrock_request(args, kwargs, usage_metadata, request_time_dt, reque
|
|
|
182
184
|
|
|
183
185
|
# Create metering call for Bedrock (pass kwargs for vision detection)
|
|
184
186
|
_create_bedrock_metering_call(
|
|
185
|
-
response, usage_metadata, request_time, response_time, request_duration, kwargs
|
|
187
|
+
response, usage_metadata, request_time, response_time, request_duration, kwargs,
|
|
188
|
+
cache_creation_tokens=cache_creation_tokens, cache_read_tokens=cache_read_tokens
|
|
186
189
|
)
|
|
187
190
|
|
|
188
191
|
return response
|
|
@@ -288,7 +291,7 @@ def _extract_trace_fields(usage_metadata, request_body=None):
|
|
|
288
291
|
}
|
|
289
292
|
|
|
290
293
|
|
|
291
|
-
def _create_bedrock_metering_call(response, usage_metadata, request_time, response_time, request_duration, request_kwargs=None):
|
|
294
|
+
def _create_bedrock_metering_call(response, usage_metadata, request_time, response_time, request_duration, request_kwargs=None, cache_creation_tokens=0, cache_read_tokens=0):
|
|
292
295
|
"""Create a metering call for Bedrock usage."""
|
|
293
296
|
|
|
294
297
|
# Get provider metadata
|
|
@@ -336,8 +339,8 @@ def _create_bedrock_metering_call(response, usage_metadata, request_time, respon
|
|
|
336
339
|
meta = extract_common_metadata(usage_metadata)
|
|
337
340
|
|
|
338
341
|
result = submit_ai_event("completion", {
|
|
339
|
-
"cache_creation_token_count":
|
|
340
|
-
"cache_read_token_count":
|
|
342
|
+
"cache_creation_token_count": cache_creation_tokens,
|
|
343
|
+
"cache_read_token_count": cache_read_tokens,
|
|
341
344
|
"input_token_cost": None,
|
|
342
345
|
"output_token_cost": None,
|
|
343
346
|
"total_cost": None,
|
|
@@ -217,18 +217,15 @@ class UnifiedReveniumCallbackHandler(AsyncCallbackHandler):
|
|
|
217
217
|
logger.debug(f"Found usage_metadata: {usage_data}")
|
|
218
218
|
|
|
219
219
|
# Check for response_metadata with token_usage (LangChain v0.1+)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
usage_data = response.response_metadata['usage']
|
|
227
|
-
if self.enable_debug_logging:
|
|
228
|
-
logger.debug(f"Found usage in response_metadata: {usage_data}")
|
|
220
|
+
if not usage_data and hasattr(response, 'response_metadata') and response.response_metadata:
|
|
221
|
+
usage_data = self._extract_usage_from_response_metadata(
|
|
222
|
+
response.response_metadata
|
|
223
|
+
)
|
|
224
|
+
if usage_data and self.enable_debug_logging:
|
|
225
|
+
logger.debug(f"Found usage in response_metadata: {usage_data}")
|
|
229
226
|
|
|
230
227
|
# Check for llm_output (older LangChain versions)
|
|
231
|
-
|
|
228
|
+
if not usage_data and hasattr(response, 'llm_output') and response.llm_output:
|
|
232
229
|
if 'token_usage' in response.llm_output:
|
|
233
230
|
usage_data = response.llm_output['token_usage']
|
|
234
231
|
if self.enable_debug_logging:
|
|
@@ -239,7 +236,7 @@ class UnifiedReveniumCallbackHandler(AsyncCallbackHandler):
|
|
|
239
236
|
logger.debug(f"Found usage in llm_output: {usage_data}")
|
|
240
237
|
|
|
241
238
|
# For streaming responses, check if there's accumulated usage data
|
|
242
|
-
|
|
239
|
+
if not usage_data and hasattr(response, 'content') and hasattr(response, 'additional_kwargs'):
|
|
243
240
|
# This might be a streaming chunk with accumulated data
|
|
244
241
|
if 'usage' in response.additional_kwargs:
|
|
245
242
|
usage_data = response.additional_kwargs['usage']
|
|
@@ -247,11 +244,17 @@ class UnifiedReveniumCallbackHandler(AsyncCallbackHandler):
|
|
|
247
244
|
logger.debug(f"Found usage in additional_kwargs: {usage_data}")
|
|
248
245
|
|
|
249
246
|
# Last resort: check for any 'usage' attribute directly
|
|
250
|
-
|
|
247
|
+
if not usage_data and hasattr(response, 'usage'):
|
|
251
248
|
usage_data = response.usage
|
|
252
249
|
if self.enable_debug_logging:
|
|
253
250
|
logger.debug(f"Found direct usage attribute: {usage_data}")
|
|
254
251
|
|
|
252
|
+
# LLMResult often stores AIMessage usage on generations[0][0].message.
|
|
253
|
+
if not usage_data:
|
|
254
|
+
usage_data = self._extract_usage_from_generations(response)
|
|
255
|
+
if usage_data and self.enable_debug_logging:
|
|
256
|
+
logger.debug(f"Found usage in generation message: {usage_data}")
|
|
257
|
+
|
|
255
258
|
if not usage_data and self.enable_debug_logging:
|
|
256
259
|
logger.debug("No usage data found in response")
|
|
257
260
|
# Log response structure for debugging
|
|
@@ -260,6 +263,44 @@ class UnifiedReveniumCallbackHandler(AsyncCallbackHandler):
|
|
|
260
263
|
|
|
261
264
|
return usage_data
|
|
262
265
|
|
|
266
|
+
def _extract_usage_from_response_metadata(
|
|
267
|
+
self, response_metadata: Dict[str, Any]
|
|
268
|
+
) -> Optional[Dict[str, Any]]:
|
|
269
|
+
if not isinstance(response_metadata, dict):
|
|
270
|
+
return None
|
|
271
|
+
if 'token_usage' in response_metadata:
|
|
272
|
+
return response_metadata['token_usage']
|
|
273
|
+
if 'usage' in response_metadata:
|
|
274
|
+
return response_metadata['usage']
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def _extract_usage_from_generations(self, response: Any) -> Optional[Dict[str, Any]]:
|
|
278
|
+
generations = getattr(response, 'generations', None)
|
|
279
|
+
if not generations:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
for generation_group in generations:
|
|
283
|
+
if isinstance(generation_group, (list, tuple)):
|
|
284
|
+
candidates = generation_group
|
|
285
|
+
else:
|
|
286
|
+
candidates = [generation_group]
|
|
287
|
+
|
|
288
|
+
for generation in candidates:
|
|
289
|
+
message = getattr(generation, 'message', None) or generation
|
|
290
|
+
|
|
291
|
+
usage_metadata = getattr(message, 'usage_metadata', None)
|
|
292
|
+
if usage_metadata:
|
|
293
|
+
return usage_metadata
|
|
294
|
+
|
|
295
|
+
response_metadata = getattr(message, 'response_metadata', None)
|
|
296
|
+
usage_data = self._extract_usage_from_response_metadata(
|
|
297
|
+
response_metadata
|
|
298
|
+
)
|
|
299
|
+
if usage_data:
|
|
300
|
+
return usage_data
|
|
301
|
+
|
|
302
|
+
return None
|
|
303
|
+
|
|
263
304
|
def _process_metering_call(self, usage_data: Dict[str, Any], run_info: Dict[str, Any]) -> None:
|
|
264
305
|
"""
|
|
265
306
|
Create a Revenium metering call with the usage data.
|
|
@@ -316,20 +357,37 @@ class UnifiedReveniumCallbackHandler(AsyncCallbackHandler):
|
|
|
316
357
|
"""
|
|
317
358
|
class MockResponse:
|
|
318
359
|
def __init__(self, usage_data, model_name):
|
|
360
|
+
def has_usage_key(key):
|
|
361
|
+
if isinstance(usage_data, dict):
|
|
362
|
+
return key in usage_data
|
|
363
|
+
return hasattr(usage_data, key)
|
|
364
|
+
|
|
365
|
+
def get_usage_value(key, default=0):
|
|
366
|
+
if isinstance(usage_data, dict):
|
|
367
|
+
return usage_data.get(key, default)
|
|
368
|
+
return getattr(usage_data, key, default)
|
|
369
|
+
|
|
370
|
+
def detail_object(details):
|
|
371
|
+
if not details:
|
|
372
|
+
return None
|
|
373
|
+
if isinstance(details, dict):
|
|
374
|
+
return type('TokenDetails', (), details)()
|
|
375
|
+
return details
|
|
376
|
+
|
|
319
377
|
# Set usage information in the format expected by middleware
|
|
320
|
-
if 'input_tokens'
|
|
378
|
+
if has_usage_key('input_tokens'):
|
|
321
379
|
# LangChain v0.2+ format
|
|
322
380
|
self.usage = type('Usage', (), {
|
|
323
|
-
'prompt_tokens':
|
|
324
|
-
'completion_tokens':
|
|
325
|
-
'total_tokens':
|
|
381
|
+
'prompt_tokens': get_usage_value('input_tokens', 0),
|
|
382
|
+
'completion_tokens': get_usage_value('output_tokens', 0),
|
|
383
|
+
'total_tokens': get_usage_value('total_tokens', 0)
|
|
326
384
|
})()
|
|
327
|
-
elif 'prompt_tokens'
|
|
385
|
+
elif has_usage_key('prompt_tokens'):
|
|
328
386
|
# OpenAI format
|
|
329
387
|
self.usage = type('Usage', (), {
|
|
330
|
-
'prompt_tokens':
|
|
331
|
-
'completion_tokens':
|
|
332
|
-
'total_tokens':
|
|
388
|
+
'prompt_tokens': get_usage_value('prompt_tokens', 0),
|
|
389
|
+
'completion_tokens': get_usage_value('completion_tokens', 0),
|
|
390
|
+
'total_tokens': get_usage_value('total_tokens', 0)
|
|
333
391
|
})()
|
|
334
392
|
else:
|
|
335
393
|
# Fallback - create minimal usage
|
|
@@ -339,6 +397,27 @@ class UnifiedReveniumCallbackHandler(AsyncCallbackHandler):
|
|
|
339
397
|
'total_tokens': 0
|
|
340
398
|
})()
|
|
341
399
|
|
|
400
|
+
# Preserve provider-specific cache detail shapes for middleware.
|
|
401
|
+
for detail_name in (
|
|
402
|
+
'prompt_tokens_details',
|
|
403
|
+
'input_tokens_details',
|
|
404
|
+
'input_token_details',
|
|
405
|
+
):
|
|
406
|
+
details = get_usage_value(detail_name, None)
|
|
407
|
+
details = detail_object(details)
|
|
408
|
+
if details is not None:
|
|
409
|
+
setattr(self.usage, detail_name, details)
|
|
410
|
+
|
|
411
|
+
# Preserve Anthropic's native flat cache keys. A live
|
|
412
|
+
# ChatAnthropic LLMResult reports cache tokens here rather than
|
|
413
|
+
# under an `input_token_details` sub-object.
|
|
414
|
+
for flat_key in (
|
|
415
|
+
'cache_creation_input_tokens',
|
|
416
|
+
'cache_read_input_tokens',
|
|
417
|
+
):
|
|
418
|
+
if has_usage_key(flat_key):
|
|
419
|
+
setattr(self.usage, flat_key, get_usage_value(flat_key, 0))
|
|
420
|
+
|
|
342
421
|
# Set model name
|
|
343
422
|
self.model = model_name
|
|
344
423
|
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/middleware.py
RENAMED
|
@@ -2,6 +2,7 @@ import datetime
|
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
import uuid
|
|
5
|
+
from numbers import Number
|
|
5
6
|
from typing import Dict, Any, Optional, Tuple
|
|
6
7
|
from enum import Enum
|
|
7
8
|
|
|
@@ -43,6 +44,73 @@ logger = logging.getLogger("revenium_middleware.extension")
|
|
|
43
44
|
SENSITIVE_FIELDS = SecurityConfig.SENSITIVE_FIELDS
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def _get_value(source: Any, key: str, default: Any = None) -> Any:
|
|
48
|
+
"""Read a value from either a dict-like or object-like SDK payload."""
|
|
49
|
+
if source is None:
|
|
50
|
+
return default
|
|
51
|
+
if isinstance(source, dict):
|
|
52
|
+
return source.get(key, default)
|
|
53
|
+
return getattr(source, key, default)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _coerce_int(value: Any, default: int = 0) -> int:
|
|
57
|
+
if value is None:
|
|
58
|
+
return default
|
|
59
|
+
if isinstance(value, bool):
|
|
60
|
+
return default
|
|
61
|
+
if isinstance(value, Number):
|
|
62
|
+
return int(value)
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
try:
|
|
65
|
+
return int(value)
|
|
66
|
+
except ValueError:
|
|
67
|
+
return default
|
|
68
|
+
return default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _extract_cache_token_counts(usage: Any) -> Tuple[int, int]:
|
|
72
|
+
"""
|
|
73
|
+
Return (cache_creation_token_count, cache_read_token_count).
|
|
74
|
+
|
|
75
|
+
OpenAI reports cached prompt/input tokens as cache reads. Anthropic-shaped
|
|
76
|
+
LangChain metadata reports read and creation counts separately.
|
|
77
|
+
"""
|
|
78
|
+
input_token_details = _get_value(usage, "input_token_details")
|
|
79
|
+
cache_creation_token_count = _coerce_int(
|
|
80
|
+
_get_value(input_token_details, "cache_creation", 0)
|
|
81
|
+
)
|
|
82
|
+
cache_read_token_count = _coerce_int(
|
|
83
|
+
_get_value(input_token_details, "cache_read", 0)
|
|
84
|
+
)
|
|
85
|
+
if cache_creation_token_count or cache_read_token_count:
|
|
86
|
+
return cache_creation_token_count, cache_read_token_count
|
|
87
|
+
|
|
88
|
+
# Anthropic native usage shape: a live ChatAnthropic LLMResult exposes the
|
|
89
|
+
# raw `response_metadata.usage` block with these flat keys rather than the
|
|
90
|
+
# normalized `input_token_details` shape.
|
|
91
|
+
cache_creation_token_count = _coerce_int(
|
|
92
|
+
_get_value(usage, "cache_creation_input_tokens", 0)
|
|
93
|
+
)
|
|
94
|
+
cache_read_token_count = _coerce_int(
|
|
95
|
+
_get_value(usage, "cache_read_input_tokens", 0)
|
|
96
|
+
)
|
|
97
|
+
if cache_creation_token_count or cache_read_token_count:
|
|
98
|
+
return cache_creation_token_count, cache_read_token_count
|
|
99
|
+
|
|
100
|
+
input_tokens_details = _get_value(usage, "input_tokens_details")
|
|
101
|
+
cache_read_token_count = _coerce_int(
|
|
102
|
+
_get_value(input_tokens_details, "cached_tokens", 0)
|
|
103
|
+
)
|
|
104
|
+
if cache_read_token_count:
|
|
105
|
+
return 0, cache_read_token_count
|
|
106
|
+
|
|
107
|
+
prompt_details = _get_value(usage, "prompt_tokens_details")
|
|
108
|
+
cache_read_token_count = _coerce_int(
|
|
109
|
+
_get_value(prompt_details, "cached_tokens", 0)
|
|
110
|
+
)
|
|
111
|
+
return 0, cache_read_token_count
|
|
112
|
+
|
|
113
|
+
|
|
46
114
|
def extract_prompt_data_if_enabled(
|
|
47
115
|
request_body: Optional[Dict[str, Any]],
|
|
48
116
|
response: Any = None,
|
|
@@ -303,10 +371,10 @@ def extract_usage_data(response, operation_type: OperationType, request_time: st
|
|
|
303
371
|
# Responses API doesn't have choices, use default
|
|
304
372
|
stop_reason = "END"
|
|
305
373
|
|
|
306
|
-
# Extract
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
374
|
+
# Extract provider-specific cache token details.
|
|
375
|
+
cache_creation_token_count, cache_read_token_count = _extract_cache_token_counts(
|
|
376
|
+
response.usage
|
|
377
|
+
)
|
|
310
378
|
|
|
311
379
|
# Build unified usage data structure
|
|
312
380
|
usage_data = {
|
|
@@ -321,8 +389,8 @@ def extract_usage_data(response, operation_type: OperationType, request_time: st
|
|
|
321
389
|
"model_source": provider_metadata["model_source"],
|
|
322
390
|
"is_streamed": False, # Will be overridden for streaming
|
|
323
391
|
"time_to_first_token": 0, # Will be set by caller if applicable
|
|
324
|
-
"cache_creation_token_count":
|
|
325
|
-
"cache_read_token_count":
|
|
392
|
+
"cache_creation_token_count": cache_creation_token_count,
|
|
393
|
+
"cache_read_token_count": cache_read_token_count,
|
|
326
394
|
"reasoning_token_count": 0,
|
|
327
395
|
"request_time": request_time,
|
|
328
396
|
"response_time": response_time,
|
|
@@ -379,6 +447,8 @@ async def log_token_usage(
|
|
|
379
447
|
input_messages: Optional[str] = None,
|
|
380
448
|
output_response: Optional[str] = None,
|
|
381
449
|
prompts_truncated: Optional[bool] = None,
|
|
450
|
+
cache_creation_token_count: Optional[int] = None,
|
|
451
|
+
cache_read_token_count: Optional[int] = None,
|
|
382
452
|
) -> None:
|
|
383
453
|
"""Log token usage to Revenium."""
|
|
384
454
|
if client is None:
|
|
@@ -403,9 +473,16 @@ async def log_token_usage(
|
|
|
403
473
|
|
|
404
474
|
# Prepare arguments for create_completion
|
|
405
475
|
# Build completion args, only including non-None values for optional fields
|
|
476
|
+
if cache_creation_token_count is None:
|
|
477
|
+
cache_creation_tokens = 0
|
|
478
|
+
else:
|
|
479
|
+
cache_creation_tokens = cache_creation_token_count
|
|
480
|
+
if cache_read_token_count is None:
|
|
481
|
+
cache_read_token_count = _coerce_int(cached_tokens)
|
|
482
|
+
|
|
406
483
|
completion_args = {
|
|
407
|
-
"cache_creation_token_count":
|
|
408
|
-
"cache_read_token_count":
|
|
484
|
+
"cache_creation_token_count": cache_creation_tokens,
|
|
485
|
+
"cache_read_token_count": cache_read_token_count,
|
|
409
486
|
"output_token_count": completion_tokens,
|
|
410
487
|
"cost_type": "AI",
|
|
411
488
|
"model": model,
|
|
@@ -634,6 +711,8 @@ def create_metering_call(
|
|
|
634
711
|
completion_tokens=usage_data["output_token_count"],
|
|
635
712
|
total_tokens=usage_data["total_token_count"],
|
|
636
713
|
cached_tokens=usage_data["cache_creation_token_count"],
|
|
714
|
+
cache_creation_token_count=usage_data["cache_creation_token_count"],
|
|
715
|
+
cache_read_token_count=usage_data["cache_read_token_count"],
|
|
637
716
|
stop_reason=usage_data["stop_reason"],
|
|
638
717
|
request_time=usage_data["request_time"],
|
|
639
718
|
response_time=usage_data["response_time"],
|
|
@@ -1044,17 +1123,17 @@ def handle_streaming_response(
|
|
|
1044
1123
|
prompt_tokens = 0
|
|
1045
1124
|
completion_tokens = 0
|
|
1046
1125
|
total_tokens = 0
|
|
1047
|
-
|
|
1126
|
+
cache_creation_token_count = 0
|
|
1127
|
+
cache_read_token_count = 0
|
|
1048
1128
|
|
|
1049
1129
|
# First check if we have the final usage data from the special chunk
|
|
1050
1130
|
if self.final_usage:
|
|
1051
1131
|
prompt_tokens = self.final_usage.prompt_tokens
|
|
1052
1132
|
completion_tokens = self.final_usage.completion_tokens
|
|
1053
1133
|
total_tokens = self.final_usage.total_tokens
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
cached_tokens = self.final_usage.prompt_tokens_details.cached_tokens
|
|
1134
|
+
cache_creation_token_count, cache_read_token_count = _extract_cache_token_counts(
|
|
1135
|
+
self.final_usage
|
|
1136
|
+
)
|
|
1058
1137
|
logger.debug(
|
|
1059
1138
|
f"Using token usage from final chunk: prompt={prompt_tokens}, completion={completion_tokens}, total={total_tokens}")
|
|
1060
1139
|
else:
|
|
@@ -1168,7 +1247,9 @@ def handle_streaming_response(
|
|
|
1168
1247
|
prompt_tokens=prompt_tokens,
|
|
1169
1248
|
completion_tokens=completion_tokens,
|
|
1170
1249
|
total_tokens=total_tokens,
|
|
1171
|
-
cached_tokens=
|
|
1250
|
+
cached_tokens=0,
|
|
1251
|
+
cache_creation_token_count=cache_creation_token_count,
|
|
1252
|
+
cache_read_token_count=cache_read_token_count,
|
|
1172
1253
|
stop_reason=stop_reason,
|
|
1173
1254
|
request_time=self.request_time_dt.strftime(
|
|
1174
1255
|
"%Y-%m-%dT%H:%M:%SZ"
|
|
@@ -1359,12 +1440,17 @@ def handle_streaming_responses(stream, request_time_dt, usage_metadata,
|
|
|
1359
1440
|
input_tokens = 0
|
|
1360
1441
|
output_tokens = 0
|
|
1361
1442
|
total_tokens = 0
|
|
1443
|
+
cache_creation_token_count = 0
|
|
1444
|
+
cache_read_token_count = 0
|
|
1362
1445
|
|
|
1363
1446
|
# Get usage data from the final chunk or last chunk
|
|
1364
1447
|
if self.final_usage:
|
|
1365
1448
|
input_tokens = self.final_usage.input_tokens
|
|
1366
1449
|
output_tokens = self.final_usage.output_tokens
|
|
1367
1450
|
total_tokens = self.final_usage.total_tokens
|
|
1451
|
+
cache_creation_token_count, cache_read_token_count = _extract_cache_token_counts(
|
|
1452
|
+
self.final_usage
|
|
1453
|
+
)
|
|
1368
1454
|
logger.debug(
|
|
1369
1455
|
f"Using token usage from Responses API stream final chunk: input={input_tokens}, "
|
|
1370
1456
|
f"output={output_tokens}, total={total_tokens}")
|
|
@@ -1373,6 +1459,9 @@ def handle_streaming_responses(stream, request_time_dt, usage_metadata,
|
|
|
1373
1459
|
input_tokens = self.last_chunk.usage.input_tokens
|
|
1374
1460
|
output_tokens = self.last_chunk.usage.output_tokens
|
|
1375
1461
|
total_tokens = self.last_chunk.usage.total_tokens
|
|
1462
|
+
cache_creation_token_count, cache_read_token_count = _extract_cache_token_counts(
|
|
1463
|
+
self.last_chunk.usage
|
|
1464
|
+
)
|
|
1376
1465
|
logger.debug(
|
|
1377
1466
|
f"Using token usage from Responses API last chunk: input={input_tokens}, "
|
|
1378
1467
|
f"output={output_tokens}, total={total_tokens}")
|
|
@@ -1463,6 +1552,8 @@ def handle_streaming_responses(stream, request_time_dt, usage_metadata,
|
|
|
1463
1552
|
completion_tokens=output_tokens,
|
|
1464
1553
|
total_tokens=total_tokens,
|
|
1465
1554
|
cached_tokens=0,
|
|
1555
|
+
cache_creation_token_count=cache_creation_token_count,
|
|
1556
|
+
cache_read_token_count=cache_read_token_count,
|
|
1466
1557
|
stop_reason="END",
|
|
1467
1558
|
request_time=self.request_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
1468
1559
|
response_time=response_time,
|
|
@@ -1541,24 +1632,11 @@ def _wrap_async_stream(stream, request_time_dt, usage_metadata, client_instance=
|
|
|
1541
1632
|
if not self.chunks and not self.final_usage:
|
|
1542
1633
|
return
|
|
1543
1634
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
cached_tokens = 0
|
|
1547
|
-
|
|
1548
|
-
if self.final_usage:
|
|
1549
|
-
prompt_tokens = getattr(self.final_usage, 'prompt_tokens', 0) or 0
|
|
1550
|
-
completion_tokens = getattr(self.final_usage, 'completion_tokens', 0) or 0
|
|
1551
|
-
if hasattr(self.final_usage, 'prompt_tokens_details') and self.final_usage.prompt_tokens_details:
|
|
1552
|
-
cached_tokens = getattr(self.final_usage.prompt_tokens_details, 'cached_tokens', 0) or 0
|
|
1635
|
+
if self.final_usage is None:
|
|
1636
|
+
return
|
|
1553
1637
|
|
|
1554
|
-
total_tokens = prompt_tokens + completion_tokens
|
|
1555
1638
|
time_to_first_token = int((self.first_token_time - request_time_dt).total_seconds() * 1000) if self.first_token_time else 0
|
|
1556
1639
|
|
|
1557
|
-
response_time_dt = datetime.datetime.now(datetime.timezone.utc)
|
|
1558
|
-
request_time = request_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1559
|
-
response_time = response_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1560
|
-
request_duration = (response_time_dt - request_time_dt).total_seconds() * 1000
|
|
1561
|
-
|
|
1562
1640
|
try:
|
|
1563
1641
|
create_metering_call(
|
|
1564
1642
|
type('Response', (), {
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8/revenium_python_sdk.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: revenium-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: The official Revenium Python SDK — unified AI metering middleware for OpenAI, Anthropic, Google, Ollama, LiteLLM, Perplexity, and fal.ai.
|
|
5
5
|
Author-email: Revenium <support@revenium.io>
|
|
6
6
|
License: MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/context.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/decorators.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/enforcement.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/metering.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/patch_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/subscriber.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/_core/trace_fields.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/agentic_outcomes.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/__init__.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/anthropic/provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/_metering.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/middleware.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/fal/trace_fields.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/types.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/common/utils.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/google/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/litellm/client/hooks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/ollama/__init__.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/ollama/middleware.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/ollama/trace_fields.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/__init__.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/azure_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/config.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/provider.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/openai/trace_fields.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/__init__.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/perplexity/provider.py
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/webhooks/__init__.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_middleware/webhooks/_verify.py
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_python_sdk.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_python_sdk.egg-info/requires.txt
RENAMED
|
File without changes
|
{revenium_python_sdk-0.1.7 → revenium_python_sdk-0.1.8}/revenium_python_sdk.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|