revenium-python-sdk 0.1.9__tar.gz → 0.2.0__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.
Files changed (132) hide show
  1. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/PKG-INFO +48 -3
  2. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/README.md +41 -1
  3. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/pyproject.toml +10 -2
  4. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/__init__.py +9 -3
  5. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/__init__.py +5 -1
  6. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/enforcement.py +91 -23
  7. revenium_python_sdk-0.2.0/revenium_middleware/_core/log_sanitize.py +100 -0
  8. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering.py +78 -1
  9. revenium_python_sdk-0.2.0/revenium_middleware/_core/metering_buffer.py +327 -0
  10. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering_submission.py +17 -3
  11. revenium_python_sdk-0.2.0/revenium_middleware/_metering/LICENSE +21 -0
  12. revenium_python_sdk-0.2.0/revenium_middleware/_metering/__init__.py +112 -0
  13. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_base_client.py +1963 -0
  14. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_client.py +513 -0
  15. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_compat.py +219 -0
  16. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_constants.py +14 -0
  17. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_exceptions.py +108 -0
  18. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_files.py +123 -0
  19. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_models.py +805 -0
  20. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_qs.py +150 -0
  21. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_resource.py +43 -0
  22. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_response.py +832 -0
  23. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_streaming.py +333 -0
  24. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_types.py +219 -0
  25. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/__init__.py +57 -0
  26. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_logs.py +25 -0
  27. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_proxy.py +65 -0
  28. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_reflection.py +42 -0
  29. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_resources_proxy.py +24 -0
  30. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_streams.py +12 -0
  31. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_sync.py +86 -0
  32. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_transform.py +447 -0
  33. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_typing.py +151 -0
  34. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_utils.py +422 -0
  35. revenium_python_sdk-0.2.0/revenium_middleware/_metering/_version.py +4 -0
  36. revenium_python_sdk-0.2.0/revenium_middleware/_metering/context.py +234 -0
  37. revenium_python_sdk-0.2.0/revenium_middleware/_metering/decorator.py +438 -0
  38. revenium_python_sdk-0.2.0/revenium_middleware/_metering/py.typed +0 -0
  39. revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/__init__.py +47 -0
  40. revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/ai.py +1870 -0
  41. revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/apis.py +459 -0
  42. revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/events.py +262 -0
  43. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/__init__.py +12 -0
  44. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_audio_params.py +203 -0
  45. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_completion_params.py +268 -0
  46. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_image_params.py +173 -0
  47. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_video_params.py +176 -0
  48. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/api_meter_request_params.py +86 -0
  49. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/api_meter_response_params.py +37 -0
  50. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/event_create_params.py +59 -0
  51. revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/metering_response_resource.py +49 -0
  52. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/agentic_outcomes.py +2 -2
  53. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/bedrock_adapter.py +14 -4
  54. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/middleware.py +195 -20
  55. revenium_python_sdk-0.2.0/revenium_middleware/anthropic/stream_create.py +176 -0
  56. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/_metering.py +2 -2
  57. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/utils.py +4 -4
  58. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/middleware.py +15 -3
  59. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/middleware.py +18 -4
  60. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/middleware.py +27 -17
  61. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/middleware.py +3 -3
  62. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/middleware.py +53 -24
  63. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/middleware.py +128 -78
  64. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/middleware.py +2 -1
  65. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/perplexity_sdk.py +2 -1
  66. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/PKG-INFO +48 -3
  67. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/SOURCES.txt +44 -0
  68. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/requires.txt +6 -1
  69. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/LICENSE +0 -0
  70. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/config.py +0 -0
  71. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/context.py +0 -0
  72. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/decorators.py +0 -0
  73. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/exceptions.py +0 -0
  74. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/fields.py +0 -0
  75. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/patch_registry.py +0 -0
  76. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
  77. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/subscriber.py +0 -0
  78. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/trace_fields.py +0 -0
  79. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/__init__.py +0 -0
  80. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/config.py +0 -0
  81. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
  82. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/provider.py +0 -0
  83. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/trace_fields.py +0 -0
  84. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/__init__.py +0 -0
  85. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/config.py +0 -0
  86. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/middleware.py +0 -0
  87. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/trace_fields.py +0 -0
  88. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/__init__.py +0 -0
  89. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/__init__.py +0 -0
  90. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/exceptions.py +0 -0
  91. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/protocols.py +0 -0
  92. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/trace_fields.py +0 -0
  93. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/types.py +0 -0
  94. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/config.py +0 -0
  95. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/__init__.py +0 -0
  96. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/provider.py +0 -0
  97. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/prompt_extractor.py +0 -0
  98. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
  99. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
  100. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/__init__.py +0 -0
  101. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/__init__.py +0 -0
  102. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/config.py +0 -0
  103. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/context.py +0 -0
  104. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/decorators.py +0 -0
  105. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/hooks.py +0 -0
  106. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
  107. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
  108. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/trace_fields.py +0 -0
  109. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/validation.py +0 -0
  110. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/__init__.py +0 -0
  111. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/__init__.py +0 -0
  112. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/trace_fields.py +0 -0
  113. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/__init__.py +0 -0
  114. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_config.py +0 -0
  115. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
  116. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/config.py +0 -0
  117. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/exceptions.py +0 -0
  118. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
  119. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
  120. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
  121. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
  122. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/provider.py +0 -0
  123. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/trace_fields.py +0 -0
  124. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/__init__.py +0 -0
  125. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/provider.py +0 -0
  126. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/trace_fields.py +0 -0
  127. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/__init__.py +0 -0
  128. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/_verify.py +0 -0
  129. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
  130. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
  131. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/setup.cfg +0 -0
  132. {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/tests/test_metering.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revenium-python-sdk
3
- Version: 0.1.9
3
+ Version: 0.2.0
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
@@ -22,7 +22,12 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Requires-Python: >=3.8
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: revenium_metering>=6.8.2
25
+ Requires-Dist: anyio<5,>=3.5.0
26
+ Requires-Dist: distro<2,>=1.7.0
27
+ Requires-Dist: httpx<1,>=0.23.0
28
+ Requires-Dist: pydantic<3,>=1.9.0
29
+ Requires-Dist: sniffio
30
+ Requires-Dist: typing-extensions<5,>=4.10
26
31
  Provides-Extra: openai
27
32
  Requires-Dist: wrapt>=1.14.0; extra == "openai"
28
33
  Requires-Dist: openai>=1.0.0; extra == "openai"
@@ -1109,7 +1114,7 @@ def free_feature(prompt: str) -> str:
1109
1114
 
1110
1115
  ## Tool Metering
1111
1116
 
1112
- The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering. Available via `revenium_metering` v6.8.2+.
1117
+ The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering.
1113
1118
 
1114
1119
  ```python
1115
1120
  import os
@@ -1311,6 +1316,44 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1311
1316
  |----------|-------------|
1312
1317
  | `REVENIUM_METERING_API_KEY` | Your Revenium API key (starts with `hak_` or `rev_`) |
1313
1318
 
1319
+ ### Configuring After Import
1320
+
1321
+ The metering client is normally built from the environment when
1322
+ `revenium_middleware` is first imported. If your credentials only become
1323
+ available later (a secrets-vault bootstrap, framework settings hooks, import
1324
+ ordering), you don't need to restart: as soon as `REVENIUM_METERING_API_KEY`
1325
+ appears in the environment, the next metered call picks it up automatically.
1326
+ You can also configure programmatically at any time:
1327
+
1328
+ ```python
1329
+ import revenium_middleware
1330
+
1331
+ revenium_middleware.initialize_metering(
1332
+ api_key="hak_your_key", # defaults to REVENIUM_METERING_API_KEY
1333
+ base_url="https://api.revenium.ai", # defaults to REVENIUM_METERING_BASE_URL
1334
+ )
1335
+ ```
1336
+
1337
+ `initialize_metering()` returns `True` when metering is enabled after the
1338
+ call; invoke it with no arguments to re-read the environment.
1339
+
1340
+ ### Delivery Resilience (Store-and-Forward)
1341
+
1342
+ Metering events that still fail after the client's own retries (network
1343
+ outages, 5xx, rate limiting) are not lost: they are held in a bounded
1344
+ in-memory buffer and replayed automatically in the background every 30
1345
+ seconds, reusing each event's original `Idempotency-Key` so replays can
1346
+ never double-bill. Permanent failures (401/403/404/422) are never buffered.
1347
+ The buffer holds up to 1000 events (oldest evicted first) for at most 24
1348
+ hours, and is drained on graceful shutdown. Inspect it programmatically:
1349
+
1350
+ ```python
1351
+ from revenium_middleware import get_buffer_stats
1352
+
1353
+ print(get_buffer_stats())
1354
+ # {'size': 0, 'max_size': 1000, 'total_buffered': 3, 'total_replayed': 3, ...}
1355
+ ```
1356
+
1314
1357
  ### Optional Environment Variables
1315
1358
 
1316
1359
  | Variable | Default | Description |
@@ -1329,6 +1372,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1329
1372
  | `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
1330
1373
  | `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
1331
1374
  | `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
1375
+ | `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
1376
+ | `REVENIUM_BUFFER_FLUSH_INTERVAL` | `30` | Seconds between automatic replay attempts for buffered events |
1332
1377
 
1333
1378
  ### Provider-Specific Environment Variables
1334
1379
 
@@ -1021,7 +1021,7 @@ def free_feature(prompt: str) -> str:
1021
1021
 
1022
1022
  ## Tool Metering
1023
1023
 
1024
- The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering. Available via `revenium_metering` v6.8.2+.
1024
+ The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering.
1025
1025
 
1026
1026
  ```python
1027
1027
  import os
@@ -1223,6 +1223,44 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1223
1223
  |----------|-------------|
1224
1224
  | `REVENIUM_METERING_API_KEY` | Your Revenium API key (starts with `hak_` or `rev_`) |
1225
1225
 
1226
+ ### Configuring After Import
1227
+
1228
+ The metering client is normally built from the environment when
1229
+ `revenium_middleware` is first imported. If your credentials only become
1230
+ available later (a secrets-vault bootstrap, framework settings hooks, import
1231
+ ordering), you don't need to restart: as soon as `REVENIUM_METERING_API_KEY`
1232
+ appears in the environment, the next metered call picks it up automatically.
1233
+ You can also configure programmatically at any time:
1234
+
1235
+ ```python
1236
+ import revenium_middleware
1237
+
1238
+ revenium_middleware.initialize_metering(
1239
+ api_key="hak_your_key", # defaults to REVENIUM_METERING_API_KEY
1240
+ base_url="https://api.revenium.ai", # defaults to REVENIUM_METERING_BASE_URL
1241
+ )
1242
+ ```
1243
+
1244
+ `initialize_metering()` returns `True` when metering is enabled after the
1245
+ call; invoke it with no arguments to re-read the environment.
1246
+
1247
+ ### Delivery Resilience (Store-and-Forward)
1248
+
1249
+ Metering events that still fail after the client's own retries (network
1250
+ outages, 5xx, rate limiting) are not lost: they are held in a bounded
1251
+ in-memory buffer and replayed automatically in the background every 30
1252
+ seconds, reusing each event's original `Idempotency-Key` so replays can
1253
+ never double-bill. Permanent failures (401/403/404/422) are never buffered.
1254
+ The buffer holds up to 1000 events (oldest evicted first) for at most 24
1255
+ hours, and is drained on graceful shutdown. Inspect it programmatically:
1256
+
1257
+ ```python
1258
+ from revenium_middleware import get_buffer_stats
1259
+
1260
+ print(get_buffer_stats())
1261
+ # {'size': 0, 'max_size': 1000, 'total_buffered': 3, 'total_replayed': 3, ...}
1262
+ ```
1263
+
1226
1264
  ### Optional Environment Variables
1227
1265
 
1228
1266
  | Variable | Default | Description |
@@ -1241,6 +1279,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1241
1279
  | `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
1242
1280
  | `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
1243
1281
  | `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
1282
+ | `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
1283
+ | `REVENIUM_BUFFER_FLUSH_INTERVAL` | `30` | Seconds between automatic replay attempts for buffered events |
1244
1284
 
1245
1285
  ### Provider-Specific Environment Variables
1246
1286
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "revenium-python-sdk"
7
- version = "0.1.9"
7
+ version = "0.2.0"
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"
@@ -13,7 +13,12 @@ authors = [
13
13
  { name = "Revenium", email = "support@revenium.io" }
14
14
  ]
15
15
  dependencies = [
16
- "revenium_metering>=6.8.2"
16
+ "anyio<5,>=3.5.0",
17
+ "distro<2,>=1.7.0",
18
+ "httpx<1,>=0.23.0",
19
+ "pydantic<3,>=1.9.0",
20
+ "sniffio",
21
+ "typing-extensions<5,>=4.10",
17
22
  ]
18
23
  keywords = ["sdk", "ai", "llm", "middleware", "metering", "revenium", "openai", "anthropic", "google", "ollama", "litellm", "perplexity", "fal"]
19
24
  classifiers = [
@@ -123,3 +128,6 @@ addopts = "-m 'not e2e and not integration'"
123
128
 
124
129
  [tool.setuptools.packages.find]
125
130
  include = ["revenium_middleware*"]
131
+
132
+ [tool.setuptools.package-data]
133
+ "revenium_middleware._metering" = ["py.typed", "LICENSE"]
@@ -138,6 +138,9 @@ logger.propagate = True
138
138
  # Existing imports like `from revenium_middleware import client` keep working.
139
139
  from ._core import ( # noqa: E402
140
140
  client,
141
+ get_buffer_stats,
142
+ get_client,
143
+ initialize_metering,
141
144
  run_async_in_thread,
142
145
  shutdown_event,
143
146
  revenium_meter,
@@ -157,8 +160,8 @@ from ._core import ( # noqa: E402
157
160
  is_selective_metering_enabled,
158
161
  )
159
162
 
160
- # Re-export tool metering utilities from revenium_metering (v6.8.2+)
161
- from revenium_metering import meter_tool, report_tool_call, configure # noqa: E402
163
+ # Re-export tool metering utilities from the in-package metering module (BACK-2151)
164
+ from ._metering import meter_tool, report_tool_call, configure # noqa: E402
162
165
 
163
166
  # Agentic-outcome client (used by examples/agentic_outcomes/ pack)
164
167
  from .agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings # noqa: E402
@@ -166,6 +169,9 @@ from .agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings # no
166
169
  __all__ = [
167
170
  # Metering exports
168
171
  "client",
172
+ "get_buffer_stats",
173
+ "get_client",
174
+ "initialize_metering",
169
175
  "run_async_in_thread",
170
176
  "shutdown_event",
171
177
  # Decorator exports
@@ -186,7 +192,7 @@ __all__ = [
186
192
  "set_idempotency_key",
187
193
  # Config exports
188
194
  "is_selective_metering_enabled",
189
- # Tool metering exports (from revenium_metering)
195
+ # Tool metering exports
190
196
  "meter_tool",
191
197
  "report_tool_call",
192
198
  "configure",
@@ -5,7 +5,8 @@ This subpackage contains the foundational components shared across all
5
5
  provider-specific middleware implementations.
6
6
  """
7
7
 
8
- from .metering import run_async_in_thread, shutdown_event, client
8
+ from .metering import run_async_in_thread, shutdown_event, client, get_client, initialize_metering
9
+ from .metering_buffer import get_buffer_stats
9
10
  from .metering_submission import submit_ai_event
10
11
  from .exceptions import BudgetExceededError
11
12
  from .enforcement import check_enforcement, is_circuit_breaker_enabled, stop_polling
@@ -50,6 +51,9 @@ from .trace_fields import (
50
51
  __all__ = [
51
52
  # Metering
52
53
  "client",
54
+ "get_client",
55
+ "initialize_metering",
56
+ "get_buffer_stats",
53
57
  "run_async_in_thread",
54
58
  "shutdown_event",
55
59
  "submit_ai_event",
@@ -10,11 +10,13 @@ Opt-in via ``REVENIUM_CIRCUIT_BREAKER_ENABLED``. Disabled by default so the
10
10
  SDK stays no-op for callers who haven't enrolled in cost controls.
11
11
  """
12
12
 
13
+ import datetime
13
14
  import json
14
15
  import logging
15
16
  import os
16
17
  import threading
17
18
  import time
19
+ from email.utils import parsedate_to_datetime
18
20
  from typing import List, Optional, Tuple
19
21
  from urllib.parse import quote, urlparse
20
22
 
@@ -27,6 +29,14 @@ logger = logging.getLogger("revenium_middleware.extension")
27
29
 
28
30
  _DEFAULT_POLL_INTERVAL = 60 # seconds between background refreshes
29
31
  _CACHE_TTL = 120 # seconds before a cached rule is considered stale
32
+
33
+ # Retry posture for the rule fetch (fleet backoff consistency): transient
34
+ # failures ride out a spike instead of leaving spend controls on stale
35
+ # limits until the next poll interval.
36
+ _FETCH_MAX_ATTEMPTS = 5
37
+ _FETCH_BACKOFF_INITIAL = 0.5 # seconds; doubles per attempt
38
+ _FETCH_BACKOFF_CAP = 8.0 # seconds; also clamps Retry-After
39
+ _FETCH_RETRYABLE_STATUS = frozenset({408, 429}) # plus any 5xx
30
40
  _RULES_CACHE_FILENAME = "revenium_enforcement_rules.json"
31
41
 
32
42
  _cached_rules: List[dict] = []
@@ -142,6 +152,37 @@ def _persist_cache_to_disk(rules: list) -> None:
142
152
  logger.debug("Failed to write enforcement cache to %s", path, exc_info=True)
143
153
 
144
154
 
155
+ def _sleep(seconds: float) -> bool:
156
+ """Backoff wait, interruptible by shutdown so the poller exits promptly.
157
+
158
+ Returns True when shutdown was signalled, so callers can abort their
159
+ retry loop instead of burning the remaining attempts with zero delay.
160
+ """
161
+ return _stop_event.wait(seconds)
162
+
163
+
164
+ def _is_retryable_status(status_code: int) -> bool:
165
+ return status_code in _FETCH_RETRYABLE_STATUS or status_code >= 500
166
+
167
+
168
+ def _retry_after_seconds(response: "httpx.Response") -> Optional[float]:
169
+ """Parse Retry-After (delta-seconds or HTTP-date), clamped to the cap."""
170
+ raw = response.headers.get("Retry-After")
171
+ if not raw:
172
+ return None
173
+ try:
174
+ delay = float(raw)
175
+ except ValueError:
176
+ try:
177
+ when = parsedate_to_datetime(raw)
178
+ if when.tzinfo is None:
179
+ when = when.replace(tzinfo=datetime.timezone.utc)
180
+ delay = (when - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
181
+ except Exception:
182
+ return None
183
+ return max(0.0, min(delay, _FETCH_BACKOFF_CAP))
184
+
185
+
145
186
  def _fetch_rules() -> Optional[list]:
146
187
  """Fetch current enforcement rules from the Revenium API.
147
188
 
@@ -170,29 +211,56 @@ def _fetch_rules() -> Optional[list]:
170
211
  # containing '/', '..', or '?' cannot retarget the request to a
171
212
  # different endpoint on the same origin.
172
213
  safe_team_id = quote(team_id, safe="")
173
- try:
174
- response = httpx.get(
175
- f"{base_url}/v2/api/ai/enforcement-rules/{safe_team_id}",
176
- headers={"x-api-key": api_key},
177
- timeout=10,
178
- )
179
- # 204 No Content == no rules configured for this team; cache empty list
180
- if response.status_code == 204:
181
- return []
182
- response.raise_for_status()
183
- data = response.json()
184
- # Server currently returns ``{"rules": [...], "compiledAt": ...}`` but
185
- # accept a bare list too so a future schema change does not silently
186
- # AttributeError its way into a stale cache.
187
- if isinstance(data, list):
188
- return data
189
- if isinstance(data, dict):
190
- return data.get("rules", [])
191
- logger.warning("Unexpected enforcement response shape: %r", type(data).__name__)
192
- return None
193
- except Exception:
194
- logger.debug("Failed to fetch enforcement rules, falling open", exc_info=True)
195
- return None
214
+ url = f"{base_url}/v2/api/ai/enforcement-rules/{safe_team_id}"
215
+ for attempt in range(_FETCH_MAX_ATTEMPTS):
216
+ backoff = min(_FETCH_BACKOFF_INITIAL * (2 ** attempt), _FETCH_BACKOFF_CAP)
217
+ try:
218
+ response = httpx.get(url, headers={"x-api-key": api_key}, timeout=10)
219
+ except (httpx.TimeoutException, httpx.TransportError):
220
+ if attempt == _FETCH_MAX_ATTEMPTS - 1:
221
+ break
222
+ if _sleep(backoff):
223
+ break
224
+ continue
225
+ except Exception:
226
+ logger.debug("Failed to fetch enforcement rules, falling open", exc_info=True)
227
+ return None
228
+
229
+ if _is_retryable_status(response.status_code):
230
+ if attempt == _FETCH_MAX_ATTEMPTS - 1:
231
+ break
232
+ delay = _retry_after_seconds(response)
233
+ if _sleep(delay if delay is not None else backoff):
234
+ break
235
+ continue
236
+
237
+ try:
238
+ # 204 No Content == no rules configured for this team; cache empty list
239
+ if response.status_code == 204:
240
+ return []
241
+ response.raise_for_status()
242
+ data = response.json()
243
+ # Server currently returns ``{"rules": [...], "compiledAt": ...}`` but
244
+ # accept a bare list too so a future schema change does not silently
245
+ # AttributeError its way into a stale cache.
246
+ if isinstance(data, list):
247
+ return data
248
+ if isinstance(data, dict):
249
+ return data.get("rules", [])
250
+ logger.warning("Unexpected enforcement response shape: %r", type(data).__name__)
251
+ return None
252
+ except Exception:
253
+ logger.debug("Failed to fetch enforcement rules, falling open", exc_info=True)
254
+ return None
255
+
256
+ # Deliberate fail-open: an enforcement-refresh outage must never become a
257
+ # customer traffic outage. The caller preserves the previous cache and the
258
+ # next poll cycle tries again.
259
+ logger.warning(
260
+ "Enforcement rule fetch exhausted %d attempts; failing open on the previous cache",
261
+ _FETCH_MAX_ATTEMPTS,
262
+ )
263
+ return None
196
264
 
197
265
 
198
266
  def _refresh_cache() -> None:
@@ -0,0 +1,100 @@
1
+ """
2
+ Shared log sanitization for all Revenium provider middlewares.
3
+
4
+ Debug logging must never emit raw request kwargs: auth material (API keys,
5
+ bearer tokens, headers) and prompt/message content (potential PII) are
6
+ redacted before anything hits application logs.
7
+
8
+ Usage:
9
+ logger.debug("Calling wrapped function with args: %s, kwargs: %s",
10
+ sanitize_for_logging(args), sanitize_for_logging(kwargs))
11
+ """
12
+
13
+ from typing import Any
14
+
15
+ __all__ = ["sanitize_for_logging"]
16
+
17
+ # Maximum recursion depth when sanitizing nested structures.
18
+ MAX_SANITIZATION_DEPTH: int = 8
19
+
20
+ # Strings longer than this are truncated (they may embed sensitive data).
21
+ MAX_LOG_STRING_LENGTH: int = 100
22
+
23
+ # Case-insensitive substring markers: any dict key containing one of these
24
+ # has its value replaced with "[REDACTED]".
25
+ SENSITIVE_KEY_MARKERS = frozenset(
26
+ {
27
+ "api_key",
28
+ "apikey",
29
+ "api-key",
30
+ "secret",
31
+ "token",
32
+ "password",
33
+ "credential",
34
+ "authorization",
35
+ "auth",
36
+ "bearer",
37
+ "private",
38
+ }
39
+ )
40
+
41
+ # Case-insensitive exact-match keys whose values carry prompt/message content
42
+ # (potential PII). Values are summarized without recursing into them.
43
+ CONTENT_FIELD_NAMES = frozenset(
44
+ {
45
+ "messages",
46
+ "prompt",
47
+ "system",
48
+ "input",
49
+ "contents",
50
+ }
51
+ )
52
+
53
+ _REDACTED = "[REDACTED]"
54
+
55
+
56
+ def _summarize_content(value: Any) -> str:
57
+ """Summarize prompt/message content without exposing (or recursing into) it."""
58
+ if isinstance(value, (list, tuple)):
59
+ return f"[REDACTED: {len(value)} items]"
60
+ return _REDACTED
61
+
62
+
63
+ def sanitize_for_logging(data: Any, max_depth: int = MAX_SANITIZATION_DEPTH) -> Any:
64
+ """
65
+ Sanitize data for secure logging.
66
+
67
+ - Values under keys containing a sensitive marker (api_key, token,
68
+ authorization, ...) are replaced with "[REDACTED]".
69
+ - Values under prompt/message content keys (messages, prompt, system,
70
+ input, contents) are summarized without recursing into them.
71
+ - Long strings are truncated to avoid leaking embedded payloads.
72
+
73
+ Args:
74
+ data: Data to sanitize (dict, list/tuple, or primitive)
75
+ max_depth: Maximum recursion depth to prevent runaway recursion
76
+
77
+ Returns:
78
+ Sanitized data safe for logging
79
+ """
80
+ if max_depth <= 0:
81
+ return "[MAX_DEPTH_REACHED]"
82
+
83
+ if isinstance(data, dict):
84
+ sanitized = {}
85
+ for key, value in data.items():
86
+ key_lower = str(key).lower()
87
+ if key_lower in CONTENT_FIELD_NAMES:
88
+ sanitized[key] = _summarize_content(value)
89
+ elif any(marker in key_lower for marker in SENSITIVE_KEY_MARKERS):
90
+ sanitized[key] = _REDACTED
91
+ else:
92
+ sanitized[key] = sanitize_for_logging(value, max_depth - 1)
93
+ return sanitized
94
+ elif isinstance(data, (list, tuple)):
95
+ return [sanitize_for_logging(item, max_depth - 1) for item in data]
96
+ elif isinstance(data, str) and len(data) > MAX_LOG_STRING_LENGTH:
97
+ # Truncate very long strings that might contain sensitive data
98
+ return data[:MAX_LOG_STRING_LENGTH] + "...[TRUNCATED]"
99
+ else:
100
+ return data
@@ -7,7 +7,7 @@ import contextvars
7
7
  import atexit
8
8
  import signal
9
9
  from typing import Literal, Awaitable, Any, Optional, Callable
10
- from revenium_metering import ReveniumMetering
10
+ from revenium_middleware._metering import ReveniumMetering
11
11
  from revenium_middleware._core.config import validate_api_key
12
12
 
13
13
  # Get the logger that was configured in __init__.py
@@ -55,6 +55,73 @@ api_key = os.environ.get("REVENIUM_METERING_API_KEY")
55
55
  _base_url_raw = os.environ.get("REVENIUM_METERING_BASE_URL")
56
56
  client = _build_metering_client(api_key, _base_url_raw)
57
57
 
58
+ # Remembers a malformed env key so the lazy path doesn't re-raise per call.
59
+ _last_failed_key: Optional[str] = None
60
+
61
+
62
+ def _sync_client_reexports(new_client: Optional[ReveniumMetering]) -> None:
63
+ """Point the well-known re-export attributes at ``new_client``.
64
+
65
+ Takes the client explicitly (rather than reading the module-level global)
66
+ so concurrent rebuilds cannot interleave and leave the re-exports pointing
67
+ at a different instance than the caller just assigned. Dynamic readers
68
+ (``revenium_middleware.client``, ``revenium_middleware._core.client``)
69
+ observe the new client. Modules that bound the name via
70
+ ``from revenium_middleware import client`` at import time keep their
71
+ snapshot -- code paths that matter resolve through ``get_client()``
72
+ instead.
73
+ """
74
+ import sys
75
+ for module_name in ("revenium_middleware", "revenium_middleware._core"):
76
+ module = sys.modules.get(module_name)
77
+ if module is not None and hasattr(module, "client"):
78
+ module.client = new_client
79
+
80
+
81
+ def initialize_metering(api_key: Optional[str] = None, base_url: Optional[str] = None) -> bool:
82
+ """(Re)build the metering client from explicit values or the environment.
83
+
84
+ Call this when credentials only become available after the first
85
+ ``revenium_middleware`` import (vault/parameter-store bootstraps, framework
86
+ settings hooks) or to reconfigure at runtime. Explicit arguments take
87
+ precedence over ``REVENIUM_METERING_API_KEY`` / ``REVENIUM_METERING_BASE_URL``.
88
+
89
+ Returns True when metering is enabled after the call. Raises ``ValueError``
90
+ for a malformed API key -- explicit configuration fails loudly, unlike the
91
+ lazy ``get_client()`` path, which logs a warning and leaves metering
92
+ disabled (best-effort by design).
93
+ """
94
+ global client
95
+ key = api_key if api_key is not None else os.environ.get("REVENIUM_METERING_API_KEY")
96
+ url = base_url if base_url is not None else os.environ.get("REVENIUM_METERING_BASE_URL")
97
+ new_client = _build_metering_client(key, url)
98
+ client = new_client
99
+ _sync_client_reexports(new_client)
100
+ return new_client is not None
101
+
102
+
103
+ def get_client() -> Optional[ReveniumMetering]:
104
+ """Return the metering client, retrying the environment when unset.
105
+
106
+ Covers env vars populated after import: as soon as
107
+ ``REVENIUM_METERING_API_KEY`` appears, the next metering event builds the
108
+ client instead of silently no-opping forever.
109
+
110
+ Best-effort by design: a malformed env key is logged once and metering
111
+ stays disabled, whereas the explicit ``initialize_metering()`` raises
112
+ ``ValueError`` so programmatic misconfiguration fails loudly.
113
+ """
114
+ global _last_failed_key
115
+ if client is None:
116
+ env_key = os.environ.get("REVENIUM_METERING_API_KEY")
117
+ if env_key and env_key != _last_failed_key:
118
+ try:
119
+ initialize_metering()
120
+ except ValueError as e:
121
+ logger.warning("Deferred metering initialization failed: %s", e)
122
+ _last_failed_key = env_key
123
+ return client
124
+
58
125
  _threads_lock = threading.Lock()
59
126
  active_threads = []
60
127
  shutdown_event = threading.Event()
@@ -78,6 +145,16 @@ def handle_exit(signum=None, frame=None):
78
145
  shutdown_event.set()
79
146
 
80
147
 
148
+ # Drain the store-and-forward buffer (bounded) before joining metering
149
+ # threads, so events that already exhausted retries get a final attempt.
150
+ try:
151
+ from revenium_middleware._core import metering_buffer
152
+
153
+ if metering_buffer._buffer is not None:
154
+ metering_buffer._buffer.flush(deadline_seconds=5.0)
155
+ except Exception as e:
156
+ logger.debug("Metering buffer drain during shutdown failed: %s", e)
157
+
81
158
  with _threads_lock:
82
159
  threads_to_join = list(active_threads)
83
160
  for thread in threads_to_join: