revenium-python-sdk 0.4.0__tar.gz → 0.6.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 (147) hide show
  1. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/PKG-INFO +102 -1
  2. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/README.md +97 -0
  3. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/pyproject.toml +7 -1
  4. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/__init__.py +17 -0
  5. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/config.py +7 -0
  6. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/fields.py +42 -0
  7. revenium_python_sdk-0.6.0/revenium_middleware/_core/load_diagnostics.py +68 -0
  8. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/metering.py +17 -9
  9. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/metering_buffer.py +42 -2
  10. revenium_python_sdk-0.6.0/revenium_middleware/_core/metering_status.py +172 -0
  11. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/metering_submission.py +20 -1
  12. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/decorator.py +15 -4
  13. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/resources/ai.py +422 -2
  14. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/ai_create_audio_params.py +54 -0
  15. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/ai_create_completion_params.py +52 -1
  16. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/ai_create_image_params.py +56 -0
  17. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/ai_create_video_params.py +54 -0
  18. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/event_create_params.py +2 -0
  19. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/__init__.py +3 -2
  20. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/fal/__init__.py +3 -2
  21. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/fal/_metering.py +13 -4
  22. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/__init__.py +4 -2
  23. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/common/utils.py +10 -0
  24. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/google_ai/__init__.py +3 -2
  25. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/google_ai/middleware.py +1 -1
  26. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/vertex_ai/__init__.py +3 -2
  27. revenium_python_sdk-0.6.0/revenium_middleware/griptape/__init__.py +35 -0
  28. revenium_python_sdk-0.6.0/revenium_middleware/griptape/_metadata.py +10 -0
  29. revenium_python_sdk-0.6.0/revenium_middleware/griptape/anthropic_driver.py +87 -0
  30. revenium_python_sdk-0.6.0/revenium_middleware/griptape/litellm_driver.py +527 -0
  31. revenium_python_sdk-0.6.0/revenium_middleware/griptape/ollama_driver.py +87 -0
  32. revenium_python_sdk-0.6.0/revenium_middleware/griptape/openai_driver.py +133 -0
  33. revenium_python_sdk-0.6.0/revenium_middleware/griptape/openai_embedding_driver.py +121 -0
  34. revenium_python_sdk-0.6.0/revenium_middleware/griptape/universal_driver.py +527 -0
  35. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/__init__.py +3 -2
  36. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/__init__.py +3 -2
  37. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/proxy/__init__.py +3 -2
  38. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/proxy/middleware.py +1 -1
  39. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/ollama/__init__.py +3 -2
  40. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/__init__.py +3 -2
  41. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/middleware.py +7 -2
  42. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/perplexity/__init__.py +14 -3
  43. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_python_sdk.egg-info/PKG-INFO +102 -1
  44. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_python_sdk.egg-info/SOURCES.txt +10 -0
  45. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_python_sdk.egg-info/requires.txt +9 -0
  46. revenium_python_sdk-0.6.0/tests/test_metering.py +411 -0
  47. revenium_python_sdk-0.4.0/tests/test_metering.py +0 -106
  48. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/LICENSE +0 -0
  49. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/__init__.py +0 -0
  50. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/context.py +0 -0
  51. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/decorators.py +0 -0
  52. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/enforcement.py +0 -0
  53. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/exceptions.py +0 -0
  54. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/log_sanitize.py +0 -0
  55. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/outcomes.py +0 -0
  56. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/patch_registry.py +0 -0
  57. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
  58. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/subscriber.py +0 -0
  59. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_core/trace_fields.py +0 -0
  60. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/LICENSE +0 -0
  61. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/__init__.py +0 -0
  62. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_base_client.py +0 -0
  63. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_client.py +0 -0
  64. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_compat.py +0 -0
  65. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_constants.py +0 -0
  66. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_exceptions.py +0 -0
  67. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_files.py +0 -0
  68. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_models.py +0 -0
  69. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_qs.py +0 -0
  70. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_resource.py +0 -0
  71. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_response.py +0 -0
  72. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_streaming.py +0 -0
  73. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_types.py +0 -0
  74. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/__init__.py +0 -0
  75. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_logs.py +0 -0
  76. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_proxy.py +0 -0
  77. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_reflection.py +0 -0
  78. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_resources_proxy.py +0 -0
  79. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_streams.py +0 -0
  80. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_sync.py +0 -0
  81. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_transform.py +0 -0
  82. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_typing.py +0 -0
  83. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_utils/_utils.py +0 -0
  84. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/_version.py +0 -0
  85. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/context.py +0 -0
  86. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/py.typed +0 -0
  87. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/resources/__init__.py +0 -0
  88. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/resources/apis.py +0 -0
  89. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/resources/events.py +0 -0
  90. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/__init__.py +0 -0
  91. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/api_meter_request_params.py +0 -0
  92. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/api_meter_response_params.py +0 -0
  93. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/_metering/types/metering_response_resource.py +0 -0
  94. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/agentic_outcomes.py +0 -0
  95. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/bedrock_adapter.py +0 -0
  96. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/bedrock_transport.py +0 -0
  97. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/config.py +0 -0
  98. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/middleware.py +0 -0
  99. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
  100. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/provider.py +0 -0
  101. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/stream_create.py +0 -0
  102. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/anthropic/trace_fields.py +0 -0
  103. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/fal/config.py +0 -0
  104. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/fal/middleware.py +0 -0
  105. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/fal/trace_fields.py +0 -0
  106. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/common/__init__.py +0 -0
  107. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/common/exceptions.py +0 -0
  108. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/common/protocols.py +0 -0
  109. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/common/trace_fields.py +0 -0
  110. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/common/types.py +0 -0
  111. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/config.py +0 -0
  112. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/google_ai/provider.py +0 -0
  113. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/prompt_extractor.py +0 -0
  114. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/vertex_ai/middleware.py +0 -0
  115. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
  116. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/job_context.py +0 -0
  117. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/job_history.py +0 -0
  118. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/config.py +0 -0
  119. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/context.py +0 -0
  120. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/decorators.py +0 -0
  121. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/hooks.py +0 -0
  122. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
  123. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
  124. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/middleware.py +0 -0
  125. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/trace_fields.py +0 -0
  126. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/litellm/client/validation.py +0 -0
  127. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/ollama/middleware.py +0 -0
  128. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/ollama/trace_fields.py +0 -0
  129. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/azure_config.py +0 -0
  130. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
  131. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/config.py +0 -0
  132. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/exceptions.py +0 -0
  133. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
  134. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
  135. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
  136. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
  137. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/provider.py +0 -0
  138. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/openai/trace_fields.py +0 -0
  139. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/perplexity/middleware.py +0 -0
  140. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/perplexity/perplexity_sdk.py +0 -0
  141. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/perplexity/provider.py +0 -0
  142. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/perplexity/trace_fields.py +0 -0
  143. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/webhooks/__init__.py +0 -0
  144. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_middleware/webhooks/_verify.py +0 -0
  145. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
  146. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
  147. {revenium_python_sdk-0.4.0 → revenium_python_sdk-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revenium-python-sdk
3
- Version: 0.4.0
3
+ Version: 0.6.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
@@ -36,6 +36,9 @@ Provides-Extra: langchain
36
36
  Requires-Dist: langchain>=0.1.16; extra == "langchain"
37
37
  Requires-Dist: langchain-openai>=0.1.0; extra == "langchain"
38
38
  Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
39
+ Provides-Extra: griptape
40
+ Requires-Dist: griptape<2.0.0,>=1.0.0; python_version >= "3.10" and extra == "griptape"
41
+ Requires-Dist: requests>=2.25.0; extra == "griptape"
39
42
  Provides-Extra: perplexity
40
43
  Requires-Dist: wrapt>=1.14.0; extra == "perplexity"
41
44
  Requires-Dist: python-dotenv>=0.19.0; extra == "perplexity"
@@ -89,6 +92,7 @@ Requires-Dist: python-dotenv>=0.19.0; extra == "dev"
89
92
  Requires-Dist: anthropic>=0.30.0; extra == "dev"
90
93
  Requires-Dist: boto3; extra == "dev"
91
94
  Requires-Dist: ollama>=0.3.0; extra == "dev"
95
+ Requires-Dist: griptape<2.0.0,>=1.0.0; python_version >= "3.10" and extra == "dev"
92
96
  Dynamic: license-file
93
97
 
94
98
  # Revenium Python SDK
@@ -132,6 +136,7 @@ The official Revenium Python SDK — unified AI metering middleware for deeply a
132
136
  | Perplexity (Native SDK) | `perplexity-native` | `pip install revenium-python-sdk[perplexity-native]` |
133
137
  | fal.ai | `fal` | `pip install revenium-python-sdk[fal]` |
134
138
  | LangChain | `langchain` | `pip install revenium-python-sdk[langchain]` |
139
+ | Griptape | `griptape` | `pip install revenium-python-sdk[griptape]` |
135
140
 
136
141
  ## Feature Matrix
137
142
 
@@ -147,6 +152,7 @@ The official Revenium Python SDK — unified AI metering middleware for deeply a
147
152
  | Terminal Summary | Yes | Yes | Yes | Yes | Yes | - | - |
148
153
  | Azure / Bedrock | Azure | Bedrock | Vertex AI | - | All | - | - |
149
154
  | LangChain Integration | Yes | - | - | - | - | - | - |
155
+ | Griptape Integration | Yes | Yes | - | Yes | Yes | - | - |
150
156
  | CrewAI Integration | - | - | - | - | Yes | - | - |
151
157
  | Proxy Mode | - | - | - | - | Yes | - | - |
152
158
 
@@ -208,6 +214,52 @@ print(response.choices[0].message.content)
208
214
 
209
215
  ---
210
216
 
217
+ ## Metering Error Visibility
218
+
219
+ Metering runs in background threads and never raises into your code path — a
220
+ metering failure will never break your AI calls. To make failures observable
221
+ anyway, the SDK provides two mechanisms:
222
+
223
+ **Subscribe to failures** with a callback:
224
+
225
+ ```python
226
+ import revenium_middleware
227
+
228
+ @revenium_middleware.on_metering_error
229
+ def alert_on_metering_failure(event):
230
+ # event.error -- the exception (e.g. an HTTP 401/500 from Revenium)
231
+ # event.operation -- "completion", "image", "tool", ... (may be None)
232
+ # event.timestamp -- UTC datetime of the failure
233
+ my_monitoring.notify(f"Revenium metering failed: {event.error}")
234
+ ```
235
+
236
+ Callbacks run on the background metering thread; exceptions they raise are
237
+ suppressed and logged, so they can never disrupt your application.
238
+
239
+ Authentication headers (`x-api-key`, `authorization`) on any HTTP
240
+ request/response attached to the exception are redacted before the error is
241
+ exposed to callbacks or `last_error`.
242
+
243
+ **Poll the status counters:**
244
+
245
+ ```python
246
+ status = revenium_middleware.get_metering_status()
247
+ print(status.success_count) # events delivered successfully
248
+ print(status.error_count) # delivery failures
249
+ print(status.last_error) # most recent exception, or None
250
+ print(status.last_error_at) # UTC datetime of the most recent failure
251
+ ```
252
+
253
+ `reset_metering_status()` zeroes the counters and clears registered
254
+ callbacks; `remove_metering_error_callback(cb)` unsubscribes a single one.
255
+
256
+ Failures are also logged at **ERROR** level on the `revenium_middleware`
257
+ logger, including HTTP 4xx/5xx responses, a missing
258
+ `REVENIUM_METERING_API_KEY`, and a provider middleware that fails to import
259
+ when the provider's SDK is installed.
260
+
261
+ ---
262
+
211
263
  ## Agentic Outcomes (Outcome-Based Metering)
212
264
 
213
265
  Emit per-agent terminal outcomes (`CONVERTED`, `DEFLECTED`, `ESCALATED`) alongside completion and tool-event records, so dashboards show business value next to AI cost.
@@ -959,6 +1011,49 @@ Credentials come from the standard environment variables (`REVENIUM_METERING_API
959
1011
 
960
1012
  ---
961
1013
 
1014
+ ### Griptape
1015
+
1016
+ Metered prompt and embedding drivers for [Griptape](https://github.com/griptape-ai/griptape) applications. Requires Python 3.10+.
1017
+
1018
+ ```bash
1019
+ pip install "revenium-python-sdk[griptape,openai]" # OpenAI
1020
+ pip install "revenium-python-sdk[griptape,anthropic]" # Anthropic
1021
+ pip install "revenium-python-sdk[griptape,ollama]" # Ollama
1022
+ pip install "revenium-python-sdk[griptape,litellm,litellm-proxy]" # 100+ providers via LiteLLM
1023
+ ```
1024
+
1025
+ `ReveniumDriver` auto-detects the provider from the model name (`gpt-*` → OpenAI, `claude-*` → Anthropic, `llama`/`mistral`/... → Ollama, anything else → LiteLLM) and wraps the matching Griptape prompt driver with Revenium metering:
1026
+
1027
+ ```python
1028
+ import os
1029
+ from griptape.structures import Agent
1030
+ from revenium_middleware.griptape import ReveniumDriver
1031
+
1032
+ os.environ["REVENIUM_METERING_API_KEY"] = "your_revenium_key"
1033
+
1034
+ agent = Agent(prompt_driver=ReveniumDriver(
1035
+ model="gpt-4o-mini",
1036
+ usage_metadata={"task_type": "demo"},
1037
+ ))
1038
+ agent.run("Hello!")
1039
+ ```
1040
+
1041
+ Force a provider with `force_provider="litellm"`, or wrap an existing driver with `ReveniumDriver(base_driver=...)`.
1042
+
1043
+ **Embeddings:**
1044
+
1045
+ ```python
1046
+ from revenium_middleware.griptape import ReveniumEmbeddingDriver
1047
+
1048
+ driver = ReveniumEmbeddingDriver(model="text-embedding-3-large")
1049
+ ```
1050
+
1051
+ **Provider-specific drivers:** for direct control, use `ReveniumOpenAiDriver`, `ReveniumAnthropicDriver`, `ReveniumOllamaDriver`, `ReveniumLiteLLMDriver` or `ReveniumOpenAiEmbeddingDriver` — each subclasses the corresponding Griptape driver and accepts a `usage_metadata` dict (see [Metadata Fields](#metadata-fields)).
1052
+
1053
+ **Migrating from `revenium-griptape`:** the standalone package is deprecated — install the `griptape` extra and change `from revenium_griptape import ReveniumDriver` to `from revenium_middleware.griptape import ReveniumDriver`. All driver class names are unchanged. One behaviour difference: the old package called `load_dotenv()` automatically at import time; the SDK never mutates your environment on import, so if you keep credentials in a `.env` file, call `load_dotenv()` yourself before creating a driver.
1054
+
1055
+ ---
1056
+
962
1057
  ## Metadata Fields
963
1058
 
964
1059
  Add business context to any API call by passing a `usage_metadata` dictionary. All fields are optional.
@@ -1026,6 +1121,12 @@ Enhanced observability fields for tracking AI operations across environments, re
1026
1121
  | `transaction_name` | `REVENIUM_TRANSACTION_NAME` | Human-friendly operation name | Label operations (e.g., `"Generate Response"`, `"Analyze Sentiment"`) |
1027
1122
  | `retry_number` | `REVENIUM_RETRY_NUMBER` | Retry attempt number (0 = first attempt) | Track retry attempts for failed operations |
1028
1123
  | `ticket_id` | `REVENIUM_TICKET_ID` | External ticket or issue ID (e.g., Jira, Linear) (max 256 chars) | Attribute AI costs to individual tickets or issues |
1124
+ | `skill_name` | `REVENIUM_SKILL_NAME` | Name of the agent skill that produced the call (max 256 chars) | Attribute AI costs to the skill that generated them |
1125
+ | `skill_source` | `REVENIUM_SKILL_SOURCE` | Where the skill was loaded from — accepted values: `bundled`, `projectSettings`, `userSettings`, `plugin` (case-sensitive) | Classify skill origin in the shared skill catalog |
1126
+ | `skill_kind` | `REVENIUM_SKILL_KIND` | Kind of skill invoked — accepted value: `workflow` (omit otherwise) | Distinguish workflow skills in reporting |
1127
+ | `skill_plugin_name` | `REVENIUM_SKILL_PLUGIN_NAME` | Plugin providing the skill, when `skill_source` is `plugin` (max 256 chars) | Attribute costs to a specific plugin |
1128
+ | `skill_marketplace_name` | `REVENIUM_SKILL_MARKETPLACE_NAME` | Marketplace the skill or plugin was installed from (max 256 chars) | Track marketplace-sourced skill usage |
1129
+ | `skill_invocation_trigger` | `REVENIUM_SKILL_INVOCATION_TRIGGER` | What triggered the skill (max 32 chars; common values: `user-slash`, `claude-proactive`, `nested-skill`) | Separate user-invoked from proactive skill usage |
1029
1130
 
1030
1131
  **Note:** `operation_type` (e.g., `CHAT`, `EMBED`, `TOOL_CALL`) and `operation_subtype` (e.g., `function_call`, `streaming`) are automatically detected by the middleware and cannot be overridden.
1031
1132
 
@@ -39,6 +39,7 @@ The official Revenium Python SDK — unified AI metering middleware for deeply a
39
39
  | Perplexity (Native SDK) | `perplexity-native` | `pip install revenium-python-sdk[perplexity-native]` |
40
40
  | fal.ai | `fal` | `pip install revenium-python-sdk[fal]` |
41
41
  | LangChain | `langchain` | `pip install revenium-python-sdk[langchain]` |
42
+ | Griptape | `griptape` | `pip install revenium-python-sdk[griptape]` |
42
43
 
43
44
  ## Feature Matrix
44
45
 
@@ -54,6 +55,7 @@ The official Revenium Python SDK — unified AI metering middleware for deeply a
54
55
  | Terminal Summary | Yes | Yes | Yes | Yes | Yes | - | - |
55
56
  | Azure / Bedrock | Azure | Bedrock | Vertex AI | - | All | - | - |
56
57
  | LangChain Integration | Yes | - | - | - | - | - | - |
58
+ | Griptape Integration | Yes | Yes | - | Yes | Yes | - | - |
57
59
  | CrewAI Integration | - | - | - | - | Yes | - | - |
58
60
  | Proxy Mode | - | - | - | - | Yes | - | - |
59
61
 
@@ -115,6 +117,52 @@ print(response.choices[0].message.content)
115
117
 
116
118
  ---
117
119
 
120
+ ## Metering Error Visibility
121
+
122
+ Metering runs in background threads and never raises into your code path — a
123
+ metering failure will never break your AI calls. To make failures observable
124
+ anyway, the SDK provides two mechanisms:
125
+
126
+ **Subscribe to failures** with a callback:
127
+
128
+ ```python
129
+ import revenium_middleware
130
+
131
+ @revenium_middleware.on_metering_error
132
+ def alert_on_metering_failure(event):
133
+ # event.error -- the exception (e.g. an HTTP 401/500 from Revenium)
134
+ # event.operation -- "completion", "image", "tool", ... (may be None)
135
+ # event.timestamp -- UTC datetime of the failure
136
+ my_monitoring.notify(f"Revenium metering failed: {event.error}")
137
+ ```
138
+
139
+ Callbacks run on the background metering thread; exceptions they raise are
140
+ suppressed and logged, so they can never disrupt your application.
141
+
142
+ Authentication headers (`x-api-key`, `authorization`) on any HTTP
143
+ request/response attached to the exception are redacted before the error is
144
+ exposed to callbacks or `last_error`.
145
+
146
+ **Poll the status counters:**
147
+
148
+ ```python
149
+ status = revenium_middleware.get_metering_status()
150
+ print(status.success_count) # events delivered successfully
151
+ print(status.error_count) # delivery failures
152
+ print(status.last_error) # most recent exception, or None
153
+ print(status.last_error_at) # UTC datetime of the most recent failure
154
+ ```
155
+
156
+ `reset_metering_status()` zeroes the counters and clears registered
157
+ callbacks; `remove_metering_error_callback(cb)` unsubscribes a single one.
158
+
159
+ Failures are also logged at **ERROR** level on the `revenium_middleware`
160
+ logger, including HTTP 4xx/5xx responses, a missing
161
+ `REVENIUM_METERING_API_KEY`, and a provider middleware that fails to import
162
+ when the provider's SDK is installed.
163
+
164
+ ---
165
+
118
166
  ## Agentic Outcomes (Outcome-Based Metering)
119
167
 
120
168
  Emit per-agent terminal outcomes (`CONVERTED`, `DEFLECTED`, `ESCALATED`) alongside completion and tool-event records, so dashboards show business value next to AI cost.
@@ -866,6 +914,49 @@ Credentials come from the standard environment variables (`REVENIUM_METERING_API
866
914
 
867
915
  ---
868
916
 
917
+ ### Griptape
918
+
919
+ Metered prompt and embedding drivers for [Griptape](https://github.com/griptape-ai/griptape) applications. Requires Python 3.10+.
920
+
921
+ ```bash
922
+ pip install "revenium-python-sdk[griptape,openai]" # OpenAI
923
+ pip install "revenium-python-sdk[griptape,anthropic]" # Anthropic
924
+ pip install "revenium-python-sdk[griptape,ollama]" # Ollama
925
+ pip install "revenium-python-sdk[griptape,litellm,litellm-proxy]" # 100+ providers via LiteLLM
926
+ ```
927
+
928
+ `ReveniumDriver` auto-detects the provider from the model name (`gpt-*` → OpenAI, `claude-*` → Anthropic, `llama`/`mistral`/... → Ollama, anything else → LiteLLM) and wraps the matching Griptape prompt driver with Revenium metering:
929
+
930
+ ```python
931
+ import os
932
+ from griptape.structures import Agent
933
+ from revenium_middleware.griptape import ReveniumDriver
934
+
935
+ os.environ["REVENIUM_METERING_API_KEY"] = "your_revenium_key"
936
+
937
+ agent = Agent(prompt_driver=ReveniumDriver(
938
+ model="gpt-4o-mini",
939
+ usage_metadata={"task_type": "demo"},
940
+ ))
941
+ agent.run("Hello!")
942
+ ```
943
+
944
+ Force a provider with `force_provider="litellm"`, or wrap an existing driver with `ReveniumDriver(base_driver=...)`.
945
+
946
+ **Embeddings:**
947
+
948
+ ```python
949
+ from revenium_middleware.griptape import ReveniumEmbeddingDriver
950
+
951
+ driver = ReveniumEmbeddingDriver(model="text-embedding-3-large")
952
+ ```
953
+
954
+ **Provider-specific drivers:** for direct control, use `ReveniumOpenAiDriver`, `ReveniumAnthropicDriver`, `ReveniumOllamaDriver`, `ReveniumLiteLLMDriver` or `ReveniumOpenAiEmbeddingDriver` — each subclasses the corresponding Griptape driver and accepts a `usage_metadata` dict (see [Metadata Fields](#metadata-fields)).
955
+
956
+ **Migrating from `revenium-griptape`:** the standalone package is deprecated — install the `griptape` extra and change `from revenium_griptape import ReveniumDriver` to `from revenium_middleware.griptape import ReveniumDriver`. All driver class names are unchanged. One behaviour difference: the old package called `load_dotenv()` automatically at import time; the SDK never mutates your environment on import, so if you keep credentials in a `.env` file, call `load_dotenv()` yourself before creating a driver.
957
+
958
+ ---
959
+
869
960
  ## Metadata Fields
870
961
 
871
962
  Add business context to any API call by passing a `usage_metadata` dictionary. All fields are optional.
@@ -933,6 +1024,12 @@ Enhanced observability fields for tracking AI operations across environments, re
933
1024
  | `transaction_name` | `REVENIUM_TRANSACTION_NAME` | Human-friendly operation name | Label operations (e.g., `"Generate Response"`, `"Analyze Sentiment"`) |
934
1025
  | `retry_number` | `REVENIUM_RETRY_NUMBER` | Retry attempt number (0 = first attempt) | Track retry attempts for failed operations |
935
1026
  | `ticket_id` | `REVENIUM_TICKET_ID` | External ticket or issue ID (e.g., Jira, Linear) (max 256 chars) | Attribute AI costs to individual tickets or issues |
1027
+ | `skill_name` | `REVENIUM_SKILL_NAME` | Name of the agent skill that produced the call (max 256 chars) | Attribute AI costs to the skill that generated them |
1028
+ | `skill_source` | `REVENIUM_SKILL_SOURCE` | Where the skill was loaded from — accepted values: `bundled`, `projectSettings`, `userSettings`, `plugin` (case-sensitive) | Classify skill origin in the shared skill catalog |
1029
+ | `skill_kind` | `REVENIUM_SKILL_KIND` | Kind of skill invoked — accepted value: `workflow` (omit otherwise) | Distinguish workflow skills in reporting |
1030
+ | `skill_plugin_name` | `REVENIUM_SKILL_PLUGIN_NAME` | Plugin providing the skill, when `skill_source` is `plugin` (max 256 chars) | Attribute costs to a specific plugin |
1031
+ | `skill_marketplace_name` | `REVENIUM_SKILL_MARKETPLACE_NAME` | Marketplace the skill or plugin was installed from (max 256 chars) | Track marketplace-sourced skill usage |
1032
+ | `skill_invocation_trigger` | `REVENIUM_SKILL_INVOCATION_TRIGGER` | What triggered the skill (max 32 chars; common values: `user-slash`, `claude-proactive`, `nested-skill`) | Separate user-invoked from proactive skill usage |
936
1033
 
937
1034
  **Note:** `operation_type` (e.g., `CHAT`, `EMBED`, `TOOL_CALL`) and `operation_subtype` (e.g., `function_call`, `streaming`) are automatically detected by the middleware and cannot be overridden.
938
1035
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "revenium-python-sdk"
7
- version = "0.4.0"
7
+ version = "0.6.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"
@@ -51,6 +51,10 @@ langchain = [
51
51
  "langchain-openai>=0.1.0",
52
52
  "langchain-core>=0.1.0",
53
53
  ]
54
+ griptape = [
55
+ "griptape>=1.0.0,<2.0.0; python_version >= '3.10'",
56
+ "requests>=2.25.0",
57
+ ]
54
58
  perplexity = [
55
59
  "wrapt>=1.14.0",
56
60
  "python-dotenv>=0.19.0",
@@ -116,9 +120,11 @@ dev = [
116
120
  "anthropic>=0.30.0",
117
121
  "boto3",
118
122
  "ollama>=0.3.0",
123
+ "griptape>=1.0.0,<2.0.0; python_version >= '3.10'",
119
124
  ]
120
125
 
121
126
  [tool.pytest.ini_options]
127
+ testpaths = ["tests"]
122
128
  markers = [
123
129
  "unit: Unit tests (fast, no external dependencies)",
124
130
  "e2e: end-to-end tests requiring live services",
@@ -160,6 +160,16 @@ from ._core import ( # noqa: E402
160
160
  is_selective_metering_enabled,
161
161
  )
162
162
 
163
+ # Metering error visibility (BACK-778): status counters + failure callbacks
164
+ from ._core.metering_status import ( # noqa: E402
165
+ MeteringErrorEvent,
166
+ MeteringStatus,
167
+ get_metering_status,
168
+ on_metering_error,
169
+ remove_metering_error_callback,
170
+ reset_metering_status,
171
+ )
172
+
163
173
  # Re-export tool metering utilities from the in-package metering module (BACK-2151)
164
174
  from ._metering import meter_tool, report_tool_call, configure # noqa: E402
165
175
 
@@ -202,6 +212,13 @@ __all__ = [
202
212
  "set_idempotency_key",
203
213
  # Config exports
204
214
  "is_selective_metering_enabled",
215
+ # Metering error visibility exports (BACK-778)
216
+ "MeteringErrorEvent",
217
+ "MeteringStatus",
218
+ "get_metering_status",
219
+ "on_metering_error",
220
+ "remove_metering_error_callback",
221
+ "reset_metering_status",
205
222
  # Tool metering exports
206
223
  "meter_tool",
207
224
  "report_tool_call",
@@ -63,6 +63,13 @@ class Config:
63
63
  ENV_REVENIUM_AGENTIC_JOB_NAME: str = "REVENIUM_AGENTIC_JOB_NAME"
64
64
  ENV_REVENIUM_AGENTIC_JOB_TYPE: str = "REVENIUM_AGENTIC_JOB_TYPE"
65
65
  ENV_REVENIUM_AGENTIC_JOB_VERSION: str = "REVENIUM_AGENTIC_JOB_VERSION"
66
+ # Skill attribution environment variables
67
+ ENV_REVENIUM_SKILL_INVOCATION_TRIGGER: str = "REVENIUM_SKILL_INVOCATION_TRIGGER"
68
+ ENV_REVENIUM_SKILL_KIND: str = "REVENIUM_SKILL_KIND"
69
+ ENV_REVENIUM_SKILL_MARKETPLACE_NAME: str = "REVENIUM_SKILL_MARKETPLACE_NAME"
70
+ ENV_REVENIUM_SKILL_NAME: str = "REVENIUM_SKILL_NAME"
71
+ ENV_REVENIUM_SKILL_PLUGIN_NAME: str = "REVENIUM_SKILL_PLUGIN_NAME"
72
+ ENV_REVENIUM_SKILL_SOURCE: str = "REVENIUM_SKILL_SOURCE"
66
73
  ENV_REVENIUM_OUTCOME_API_KEY: str = "REVENIUM_OUTCOME_API_KEY"
67
74
  ENV_REVENIUM_PROFITSTREAM_BASE_URL: str = "REVENIUM_PROFITSTREAM_BASE_URL"
68
75
 
@@ -30,6 +30,26 @@ _AGENTIC_JOB_ENV_MAP = {
30
30
  "agenticJobVersion": Config.ENV_REVENIUM_AGENTIC_JOB_VERSION,
31
31
  }
32
32
 
33
+ # Keys are the typed snake_case params on create_completion; values are the
34
+ # accepted usage_metadata aliases in precedence order.
35
+ SKILL_FIELD_MAP = {
36
+ "skill_invocation_trigger": ("skill_invocation_trigger", "skillInvocationTrigger"),
37
+ "skill_kind": ("skill_kind", "skillKind"),
38
+ "skill_marketplace_name": ("skill_marketplace_name", "skillMarketplaceName"),
39
+ "skill_name": ("skill_name", "skillName"),
40
+ "skill_plugin_name": ("skill_plugin_name", "skillPluginName"),
41
+ "skill_source": ("skill_source", "skillSource"),
42
+ }
43
+
44
+ _SKILL_ENV_MAP = {
45
+ "skill_invocation_trigger": Config.ENV_REVENIUM_SKILL_INVOCATION_TRIGGER,
46
+ "skill_kind": Config.ENV_REVENIUM_SKILL_KIND,
47
+ "skill_marketplace_name": Config.ENV_REVENIUM_SKILL_MARKETPLACE_NAME,
48
+ "skill_name": Config.ENV_REVENIUM_SKILL_NAME,
49
+ "skill_plugin_name": Config.ENV_REVENIUM_SKILL_PLUGIN_NAME,
50
+ "skill_source": Config.ENV_REVENIUM_SKILL_SOURCE,
51
+ }
52
+
33
53
 
34
54
  def extract_field_with_fallback(
35
55
  source: Mapping[str, Any],
@@ -129,6 +149,28 @@ def extract_agentic_job_fields(source: Mapping[str, Any]) -> Dict[str, Any]:
129
149
  return result
130
150
 
131
151
 
152
+ def extract_skill_fields(source: Mapping[str, Any]) -> Dict[str, Any]:
153
+ """Resolve skill attribution fields with per-field precedence:
154
+
155
+ explicit source metadata (snake then camel alias) >
156
+ ``REVENIUM_SKILL_*`` env var. Each field resolves independently and
157
+ absent fields are omitted entirely (never emitted as None). Returns
158
+ snake_case keys matching the typed create_completion parameters.
159
+ """
160
+ result = {}
161
+ for param_name, aliases in SKILL_FIELD_MAP.items():
162
+ value = None
163
+ for alias in aliases:
164
+ value = source.get(alias)
165
+ if value is not None:
166
+ break
167
+ if value is None:
168
+ value = os.getenv(_SKILL_ENV_MAP[param_name]) or None
169
+ if value is not None:
170
+ result[param_name] = value
171
+ return result
172
+
173
+
132
174
  def merge_extra_body(existing: Optional[Dict[str, Any]], agentic_fields: Dict[str, Any]) -> Optional[Dict[str, Any]]:
133
175
  if not agentic_fields:
134
176
  return existing if existing else None
@@ -0,0 +1,68 @@
1
+ """Log-level policy for provider middleware load failures.
2
+
3
+ Every provider package guards its middleware import with ``except
4
+ ImportError`` so that optional providers can be absent. That guard used to
5
+ log at DEBUG unconditionally, which hid real breakage: a customer with the
6
+ provider SDK installed whose middleware failed to import (missing ``wrapt``,
7
+ a broken internal import, a version conflict) got zero metering with no
8
+ visible signal.
9
+
10
+ The policy here distinguishes the two cases:
11
+
12
+ - Provider SDK **installed** but middleware import failed → ERROR, including
13
+ the underlying exception. Metering was expected to work and silently won't.
14
+ - Provider SDK **absent** → DEBUG. The provider simply isn't in use (e.g.
15
+ ``revenium_middleware.griptape`` imports several provider middlewares
16
+ eagerly and most installs only have one provider SDK).
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import importlib.util
21
+ import logging
22
+ from typing import Tuple
23
+
24
+ logger = logging.getLogger("revenium_middleware")
25
+
26
+
27
+ def _package_installed(package: str) -> bool:
28
+ try:
29
+ return importlib.util.find_spec(package) is not None
30
+ except ModuleNotFoundError:
31
+ # find_spec("google.genai") raises when the parent package is absent.
32
+ return False
33
+ except Exception:
34
+ # Any other spec-lookup failure means the package exists but is
35
+ # broken — fail toward visibility.
36
+ return True
37
+
38
+
39
+ def log_middleware_load_failure(
40
+ provider_label: str,
41
+ exc: BaseException,
42
+ required_packages: Tuple[str, ...],
43
+ ) -> None:
44
+ """Log a provider middleware import failure at the appropriate level.
45
+
46
+ Args:
47
+ provider_label: Human-readable provider name for the log message.
48
+ exc: The ImportError (or subclass) that aborted the middleware load.
49
+ required_packages: Import names of the provider's own SDK package(s).
50
+ If any of them is installed, the failure is unexpected and logged
51
+ at ERROR; if none are, the provider is not in use and the failure
52
+ is logged at DEBUG.
53
+ """
54
+ if any(_package_installed(pkg) for pkg in required_packages):
55
+ logger.error(
56
+ "Revenium %s middleware could not load: %s. "
57
+ "Metering will NOT be active for %s calls.",
58
+ provider_label,
59
+ exc,
60
+ provider_label,
61
+ )
62
+ else:
63
+ logger.debug(
64
+ "%s SDK not installed (%s); Revenium %s middleware not loaded",
65
+ provider_label,
66
+ exc,
67
+ provider_label,
68
+ )
@@ -22,7 +22,8 @@ def _build_metering_client(
22
22
  ) -> Optional[ReveniumMetering]:
23
23
  """Construct a ReveniumMetering client from raw env-var inputs.
24
24
 
25
- Returns ``None`` when the API key is missing/empty (logs a warning).
25
+ Returns ``None`` when the API key is missing/empty (logs an error --
26
+ metering silently disabled is the failure mode this SDK must surface).
26
27
  Raises ``ValueError`` when the API key is present but malformed.
27
28
  Falls back to the library default when ``base_url_raw`` is set but not
28
29
  a valid http(s) URL.
@@ -40,7 +41,10 @@ def _build_metering_client(
40
41
  )
41
42
 
42
43
  if not api_key:
43
- logger.warning("Metering API key not set -- metering is disabled")
44
+ logger.error(
45
+ "REVENIUM_METERING_API_KEY environment variable is not set. "
46
+ "Metering is disabled and no usage data will be sent to Revenium."
47
+ )
44
48
  return None
45
49
 
46
50
  validate_api_key(api_key)
@@ -86,10 +90,11 @@ def initialize_metering(api_key: Optional[str] = None, base_url: Optional[str] =
86
90
  settings hooks) or to reconfigure at runtime. Explicit arguments take
87
91
  precedence over ``REVENIUM_METERING_API_KEY`` / ``REVENIUM_METERING_BASE_URL``.
88
92
 
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
+ Returns True when metering is enabled after the call; a missing/empty key
94
+ logs an ERROR and leaves metering disabled. Raises ``ValueError`` for a
95
+ malformed API key -- explicit configuration fails loudly, unlike the lazy
96
+ ``get_client()`` path, which logs a warning once for a malformed env key
97
+ and leaves metering disabled.
93
98
  """
94
99
  global client
95
100
  key = api_key if api_key is not None else os.environ.get("REVENIUM_METERING_API_KEY")
@@ -107,8 +112,9 @@ def get_client() -> Optional[ReveniumMetering]:
107
112
  ``REVENIUM_METERING_API_KEY`` appears, the next metering event builds the
108
113
  client instead of silently no-opping forever.
109
114
 
110
- Best-effort by design: a malformed env key is logged once and metering
111
- stays disabled, whereas the explicit ``initialize_metering()`` raises
115
+ A missing/unset key is logged at ERROR level by the build path; a
116
+ malformed env key is logged once as a warning and metering stays
117
+ disabled, whereas the explicit ``initialize_metering()`` raises
112
118
  ``ValueError`` so programmatic misconfiguration fails loudly.
113
119
  """
114
120
  global _last_failed_key
@@ -240,8 +246,10 @@ class MeteringThread(threading.Thread):
240
246
  # Log errors unless it's during shutdown
241
247
  if not shutdown_event.is_set():
242
248
  self.error = e
249
+ from revenium_middleware._core.metering_status import record_metering_error
250
+ record_metering_error(e)
243
251
  # Use exc_info=True to include traceback in the log
244
- logger.warning(f"Error in metering thread {self.name}: {str(e)}", exc_info=True)
252
+ logger.error(f"Error in metering thread {self.name}: {str(e)}", exc_info=True)
245
253
  else:
246
254
  logger.debug(f"Exception ignored in metering thread {self.name} during shutdown: {str(e)}")
247
255
  finally:
@@ -13,10 +13,15 @@ import os
13
13
  import threading
14
14
  import time
15
15
  from collections import deque
16
- from typing import Any, Callable, Deque, Dict, Optional
16
+ from typing import Any, Callable, Deque, Dict, List, Optional, Tuple
17
17
 
18
18
  import httpx
19
19
 
20
+ from revenium_middleware._core.metering_status import (
21
+ record_metering_error,
22
+ record_metering_success,
23
+ )
24
+
20
25
  logger = logging.getLogger("revenium_middleware")
21
26
 
22
27
  DEFAULT_MAX_SIZE = 1000
@@ -196,12 +201,18 @@ class MeteringBuffer:
196
201
  """
197
202
  sent = expired = discarded = 0
198
203
  started = time.monotonic()
204
+ # Status recording is deferred until _flush_lock is released:
205
+ # record_metering_error() runs subscriber callbacks synchronously, and
206
+ # a callback that calls back into this buffer would self-deadlock on
207
+ # the non-reentrant lock.
208
+ deferred_outcomes: List[Optional[Tuple[BaseException, str]]] = []
199
209
 
200
210
  with self._flush_lock:
201
211
  while True:
202
212
  if deadline_seconds is not None and time.monotonic() - started >= deadline_seconds:
203
213
  break
204
214
 
215
+ expired_kind: Optional[str] = None
205
216
  with self._lock:
206
217
  if not self._events:
207
218
  break
@@ -210,7 +221,19 @@ class MeteringBuffer:
210
221
  self._events.popleft()
211
222
  self._total_expired += 1
212
223
  expired += 1
213
- continue
224
+ expired_kind = event.kind
225
+ if expired_kind is not None:
226
+ # Expiry is a terminal failure with no delivery exception
227
+ # in hand; synthesize one so status counters and
228
+ # on_metering_error subscribers still get the signal.
229
+ deferred_outcomes.append((
230
+ TimeoutError(
231
+ f"buffered {expired_kind} metering event expired after "
232
+ f"{self._max_age_seconds:.0f}s without successful replay"
233
+ ),
234
+ expired_kind,
235
+ ))
236
+ continue
214
237
 
215
238
  # Cap each replay call so a single slow network call cannot
216
239
  # blow through the flush deadline (e.g. the shutdown budget).
@@ -232,16 +255,23 @@ class MeteringBuffer:
232
255
  # Only count the discard if the event is still at the
233
256
  # front; a concurrent push at capacity may have evicted
234
257
  # (and counted) it already.
258
+ discarded_here = False
235
259
  with self._lock:
236
260
  if self._events and self._events[0] is event:
237
261
  self._events.popleft()
238
262
  self._total_discarded += 1
239
263
  discarded += 1
264
+ discarded_here = True
265
+ if discarded_here:
266
+ # Terminal failure: the event is gone for good, so
267
+ # surface it to status counters and subscribers.
268
+ deferred_outcomes.append((exc, event.kind))
240
269
  logger.debug("Discarded buffered event after permanent failure: %s", exc)
241
270
  continue
242
271
 
243
272
  # Same identity guard: only count the replay if we actually
244
273
  # popped this event (not concurrently evicted-and-counted).
274
+ replayed_here = False
245
275
  with self._lock:
246
276
  if self._events and self._events[0] is event:
247
277
  self._events.popleft()
@@ -249,6 +279,16 @@ class MeteringBuffer:
249
279
  if len(self._events) < self._max_size:
250
280
  self._was_full = False
251
281
  sent += 1
282
+ replayed_here = True
283
+ if replayed_here:
284
+ deferred_outcomes.append(None)
285
+
286
+ for outcome in deferred_outcomes:
287
+ if outcome is None:
288
+ record_metering_success()
289
+ else:
290
+ error, kind = outcome
291
+ record_metering_error(error, operation=kind)
252
292
 
253
293
  remaining = self.stats()["size"]
254
294
  if sent or expired or discarded: