revenium-python-sdk 0.1.10__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.10/revenium_python_sdk.egg-info → revenium_python_sdk-0.2.0}/PKG-INFO +41 -1
  2. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/README.md +40 -0
  3. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/pyproject.toml +1 -1
  4. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/__init__.py +6 -0
  5. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/__init__.py +5 -1
  6. {revenium_python_sdk-0.1.10 → 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.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering.py +77 -0
  9. revenium_python_sdk-0.2.0/revenium_middleware/_core/metering_buffer.py +327 -0
  10. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering_submission.py +17 -3
  11. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/decorator.py +101 -45
  12. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/bedrock_adapter.py +14 -4
  13. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/middleware.py +195 -20
  14. revenium_python_sdk-0.2.0/revenium_middleware/anthropic/stream_create.py +176 -0
  15. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/_metering.py +2 -2
  16. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/utils.py +4 -4
  17. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/middleware.py +15 -3
  18. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/middleware.py +18 -4
  19. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/middleware.py +27 -17
  20. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/middleware.py +3 -3
  21. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/middleware.py +53 -24
  22. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/middleware.py +128 -78
  23. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/middleware.py +2 -1
  24. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/perplexity_sdk.py +2 -1
  25. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0/revenium_python_sdk.egg-info}/PKG-INFO +41 -1
  26. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/SOURCES.txt +3 -0
  27. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/LICENSE +0 -0
  28. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/config.py +0 -0
  29. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/context.py +0 -0
  30. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/decorators.py +0 -0
  31. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/exceptions.py +0 -0
  32. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/fields.py +0 -0
  33. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/patch_registry.py +0 -0
  34. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
  35. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/subscriber.py +0 -0
  36. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/trace_fields.py +0 -0
  37. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/LICENSE +0 -0
  38. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/__init__.py +0 -0
  39. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_base_client.py +0 -0
  40. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_client.py +0 -0
  41. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_compat.py +0 -0
  42. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_constants.py +0 -0
  43. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_exceptions.py +0 -0
  44. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_files.py +0 -0
  45. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_models.py +0 -0
  46. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_qs.py +0 -0
  47. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_resource.py +0 -0
  48. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_response.py +0 -0
  49. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_streaming.py +0 -0
  50. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_types.py +0 -0
  51. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/__init__.py +0 -0
  52. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_logs.py +0 -0
  53. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_proxy.py +0 -0
  54. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_reflection.py +0 -0
  55. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_resources_proxy.py +0 -0
  56. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_streams.py +0 -0
  57. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_sync.py +0 -0
  58. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_transform.py +0 -0
  59. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_typing.py +0 -0
  60. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_utils.py +0 -0
  61. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_version.py +0 -0
  62. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/context.py +0 -0
  63. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/py.typed +0 -0
  64. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/__init__.py +0 -0
  65. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/ai.py +0 -0
  66. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/apis.py +0 -0
  67. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/events.py +0 -0
  68. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/__init__.py +0 -0
  69. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_audio_params.py +0 -0
  70. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_completion_params.py +0 -0
  71. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_image_params.py +0 -0
  72. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_video_params.py +0 -0
  73. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/api_meter_request_params.py +0 -0
  74. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/api_meter_response_params.py +0 -0
  75. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/event_create_params.py +0 -0
  76. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/metering_response_resource.py +0 -0
  77. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/agentic_outcomes.py +0 -0
  78. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/__init__.py +0 -0
  79. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/config.py +0 -0
  80. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
  81. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/provider.py +0 -0
  82. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/trace_fields.py +0 -0
  83. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/__init__.py +0 -0
  84. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/config.py +0 -0
  85. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/middleware.py +0 -0
  86. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/trace_fields.py +0 -0
  87. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/__init__.py +0 -0
  88. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/__init__.py +0 -0
  89. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/exceptions.py +0 -0
  90. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/protocols.py +0 -0
  91. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/trace_fields.py +0 -0
  92. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/types.py +0 -0
  93. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/config.py +0 -0
  94. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/__init__.py +0 -0
  95. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/provider.py +0 -0
  96. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/prompt_extractor.py +0 -0
  97. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
  98. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
  99. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/__init__.py +0 -0
  100. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/__init__.py +0 -0
  101. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/config.py +0 -0
  102. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/context.py +0 -0
  103. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/decorators.py +0 -0
  104. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/hooks.py +0 -0
  105. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
  106. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
  107. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/trace_fields.py +0 -0
  108. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/validation.py +0 -0
  109. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/__init__.py +0 -0
  110. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/__init__.py +0 -0
  111. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/trace_fields.py +0 -0
  112. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/__init__.py +0 -0
  113. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_config.py +0 -0
  114. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
  115. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/config.py +0 -0
  116. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/exceptions.py +0 -0
  117. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
  118. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
  119. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
  120. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
  121. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/provider.py +0 -0
  122. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/trace_fields.py +0 -0
  123. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/__init__.py +0 -0
  124. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/provider.py +0 -0
  125. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/trace_fields.py +0 -0
  126. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/__init__.py +0 -0
  127. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/_verify.py +0 -0
  128. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
  129. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/requires.txt +0 -0
  130. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
  131. {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/setup.cfg +0 -0
  132. {revenium_python_sdk-0.1.10 → 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.10
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
@@ -1316,6 +1316,44 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1316
1316
  |----------|-------------|
1317
1317
  | `REVENIUM_METERING_API_KEY` | Your Revenium API key (starts with `hak_` or `rev_`) |
1318
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
+
1319
1357
  ### Optional Environment Variables
1320
1358
 
1321
1359
  | Variable | Default | Description |
@@ -1334,6 +1372,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1334
1372
  | `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
1335
1373
  | `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
1336
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 |
1337
1377
 
1338
1378
  ### Provider-Specific Environment Variables
1339
1379
 
@@ -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.10"
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"
@@ -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,
@@ -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
@@ -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
@@ -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: