revenium-python-sdk 0.7.0__tar.gz → 0.8.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 (150) hide show
  1. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/PKG-INFO +127 -7
  2. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/README.md +126 -6
  3. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/pyproject.toml +1 -1
  4. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/__init__.py +20 -0
  5. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/config.py +23 -0
  6. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/context.py +30 -1
  7. revenium_python_sdk-0.8.0/revenium_middleware/_core/enforcement.py +1348 -0
  8. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/exceptions.py +22 -5
  9. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/fields.py +29 -0
  10. revenium_python_sdk-0.8.0/revenium_middleware/_core/outcomes.py +728 -0
  11. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/trace_fields.py +82 -0
  12. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/ai.py +32 -0
  13. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_audio_params.py +3 -0
  14. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_completion_params.py +3 -0
  15. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_image_params.py +3 -0
  16. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_video_params.py +3 -0
  17. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/agentic_outcomes.py +102 -7
  18. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/bedrock_transport.py +1 -0
  19. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/middleware.py +8 -0
  20. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/trace_fields.py +3 -0
  21. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/_metering.py +10 -0
  22. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/trace_fields.py +3 -0
  23. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/trace_fields.py +3 -0
  24. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/utils.py +12 -0
  25. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/job_context.py +272 -37
  26. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/job_history.py +8 -10
  27. revenium_python_sdk-0.8.0/revenium_middleware/job_type_economics.py +214 -0
  28. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/middleware.py +4 -0
  29. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/trace_fields.py +3 -0
  30. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/ollama/middleware.py +5 -0
  31. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/ollama/trace_fields.py +3 -0
  32. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/middleware.py +13 -3
  33. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/trace_fields.py +3 -0
  34. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/middleware.py +4 -1
  35. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/perplexity_sdk.py +4 -1
  36. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/trace_fields.py +3 -0
  37. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/PKG-INFO +127 -7
  38. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/SOURCES.txt +1 -0
  39. revenium_python_sdk-0.7.0/revenium_middleware/_core/enforcement.py +0 -817
  40. revenium_python_sdk-0.7.0/revenium_middleware/_core/outcomes.py +0 -424
  41. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/LICENSE +0 -0
  42. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/__init__.py +0 -0
  43. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/cache_tokens.py +0 -0
  44. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/decorators.py +0 -0
  45. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/load_diagnostics.py +0 -0
  46. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/log_sanitize.py +0 -0
  47. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering.py +0 -0
  48. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering_buffer.py +0 -0
  49. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering_status.py +0 -0
  50. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering_submission.py +0 -0
  51. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/patch_registry.py +0 -0
  52. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
  53. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/subscriber.py +0 -0
  54. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/LICENSE +0 -0
  55. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/__init__.py +0 -0
  56. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_base_client.py +0 -0
  57. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_client.py +0 -0
  58. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_compat.py +0 -0
  59. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_constants.py +0 -0
  60. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_exceptions.py +0 -0
  61. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_files.py +0 -0
  62. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_models.py +0 -0
  63. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_qs.py +0 -0
  64. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_resource.py +0 -0
  65. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_response.py +0 -0
  66. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_streaming.py +0 -0
  67. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_types.py +0 -0
  68. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/__init__.py +0 -0
  69. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_logs.py +0 -0
  70. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_proxy.py +0 -0
  71. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_reflection.py +0 -0
  72. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_resources_proxy.py +0 -0
  73. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_streams.py +0 -0
  74. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_sync.py +0 -0
  75. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_transform.py +0 -0
  76. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_typing.py +0 -0
  77. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_utils.py +0 -0
  78. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_version.py +0 -0
  79. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/context.py +0 -0
  80. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/decorator.py +0 -0
  81. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/py.typed +0 -0
  82. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/__init__.py +0 -0
  83. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/apis.py +0 -0
  84. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/events.py +0 -0
  85. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/__init__.py +0 -0
  86. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/api_meter_request_params.py +0 -0
  87. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/api_meter_response_params.py +0 -0
  88. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/event_create_params.py +0 -0
  89. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/metering_response_resource.py +0 -0
  90. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/__init__.py +0 -0
  91. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/bedrock_adapter.py +0 -0
  92. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/config.py +0 -0
  93. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
  94. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/provider.py +0 -0
  95. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/stream_create.py +0 -0
  96. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/__init__.py +0 -0
  97. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/config.py +0 -0
  98. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/middleware.py +0 -0
  99. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/__init__.py +0 -0
  100. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/__init__.py +0 -0
  101. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/exceptions.py +0 -0
  102. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/protocols.py +0 -0
  103. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/types.py +0 -0
  104. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/config.py +0 -0
  105. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/google_ai/__init__.py +0 -0
  106. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/google_ai/middleware.py +0 -0
  107. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/google_ai/provider.py +0 -0
  108. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/prompt_extractor.py +0 -0
  109. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
  110. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/vertex_ai/middleware.py +0 -0
  111. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
  112. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/__init__.py +0 -0
  113. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/_metadata.py +0 -0
  114. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/anthropic_driver.py +0 -0
  115. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/litellm_driver.py +0 -0
  116. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/ollama_driver.py +0 -0
  117. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/openai_driver.py +0 -0
  118. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/openai_embedding_driver.py +0 -0
  119. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/universal_driver.py +0 -0
  120. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/__init__.py +0 -0
  121. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/__init__.py +0 -0
  122. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/config.py +0 -0
  123. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/context.py +0 -0
  124. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/decorators.py +0 -0
  125. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/hooks.py +0 -0
  126. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
  127. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
  128. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/validation.py +0 -0
  129. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/proxy/__init__.py +0 -0
  130. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/proxy/middleware.py +0 -0
  131. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/ollama/__init__.py +0 -0
  132. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/__init__.py +0 -0
  133. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/azure_config.py +0 -0
  134. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
  135. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/config.py +0 -0
  136. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/exceptions.py +0 -0
  137. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
  138. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
  139. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
  140. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
  141. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/provider.py +0 -0
  142. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/__init__.py +0 -0
  143. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/provider.py +0 -0
  144. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/webhooks/__init__.py +0 -0
  145. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/webhooks/_verify.py +0 -0
  146. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
  147. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/requires.txt +0 -0
  148. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
  149. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/setup.cfg +0 -0
  150. {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.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.7.0
3
+ Version: 0.8.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
@@ -264,7 +264,63 @@ when the provider's SDK is installed.
264
264
 
265
265
  Emit per-agent terminal outcomes (`CONVERTED`, `DEFLECTED`, `ESCALATED`) alongside completion and tool-event records, so dashboards show business value next to AI cost.
266
266
 
267
- > **You need a write-scope key (`rev_sk_`) to use the agentic outcomes API.** Metering keys (`rev_mk_`) can only meter completions and tool events — they cannot report, amend, or read job outcomes, and the SDK rejects them client-side before any HTTP request is made. Key resolution: explicit `api_key=` > `REVENIUM_OUTCOME_API_KEY` > `REVENIUM_METERING_API_KEY`.
267
+ > **You need a write-scope key (`rev_sk_`) to use the agentic outcomes API.** Metering keys (`rev_mk_`) can only meter completions and tool events — they cannot report, amend, or read job outcomes, and the SDK rejects them client-side before any HTTP request is made. Key resolution: explicit `api_key=` > `REVENIUM_WRITE_API_KEY` > `REVENIUM_OUTCOME_API_KEY` (deprecated fallback) > `REVENIUM_METERING_API_KEY`.
268
+
269
+ ### Job-Type Economics and Outcome Facts
270
+
271
+ Keep a metering key for AI telemetry and a separate write key for outcomes and
272
+ job-type configuration. A registered `valuePerUnit` rule takes precedence over
273
+ an outcome's `outcome_value`; the backend never sums the two value sources.
274
+
275
+ ```python
276
+ from revenium_middleware import (
277
+ Baseline, JobTypeEconomics, PeriodFactEntry, create_baseline,
278
+ report_period_facts, upsert_job_type_economics,
279
+ )
280
+
281
+ upsert_job_type_economics("claim", JobTypeEconomics(
282
+ unit_metric_key="completed_claims", unit_label="claim",
283
+ metrics=[{
284
+ "key": "completed_claims", "type": "COUNT",
285
+ "direction": "HIGHER_IS_BETTER", "aggregation": "SUM",
286
+ "resolution": "PER_JOB",
287
+ }],
288
+ dimensions=[{"key": "region", "allowedValues": ["us", "ca"]}],
289
+ monetization={
290
+ "metricKey": "completed_claims", "valuePerUnit": 4.25,
291
+ "currency": "USD", "category": "COST_AVOIDED", "basis": "REALIZED",
292
+ },
293
+ ))
294
+ create_baseline("claim", Baseline(
295
+ effective_from="2026-08-01T00:00:00Z", cost_per_unit=4.25, currency="USD",
296
+ ))
297
+ report_period_facts("claim", [PeriodFactEntry(
298
+ period_start="2026-08-01T00:00:00Z", period_end="2026-09-01T00:00:00Z",
299
+ dimension_key="region", dimension_value="us",
300
+ key="completed_claims", value=1280,
301
+ )])
302
+ ```
303
+
304
+ `effective_from` is the only required field on a baseline; every other field
305
+ is optional, and a baseline without it is rejected. A job type must be
306
+ declared with `upsert_job_type_economics` before it accepts baselines or
307
+ facts, and `report_period_facts` accepts only metrics declared with
308
+ `"resolution": "PERIOD"`.
309
+
310
+ Job economics currency values must be USD. Baselines and period facts use
311
+ server-supplied attribution when their provenance,
312
+ reporter, and source fields are omitted. Set those fields only when you need an
313
+ explicit override. Economics metric directions are
314
+ `HIGHER_IS_BETTER` or `LOWER_IS_BETTER`; monetization categories are
315
+ `REVENUE`, `COST_AVOIDED`, `TIME_SAVED`, and `LEADING_VALUE`, with a
316
+ `REALIZED` or `EXPECTED` basis.
317
+
318
+ Use `CUSTOMER_DECLARED` or `MEASURED` for a baseline override. Use `MEASURED`,
319
+ `SELF_REPORTED`, or `DERIVED` for a period fact override.
320
+
321
+ Facts are append-only and keyed on the period, dimension and metric key
322
+ together. Re-appending that tuple supersedes the active fact, and the server
323
+ requires `reason=` on the entry when it does.
268
324
 
269
325
  ### JobContext
270
326
 
@@ -311,10 +367,70 @@ history = get_outcome_history("sales-lead-8842")
311
367
  # List[JobOutcomeAmendment], ordered by amendment_sequence (1 = the initial report)
312
368
  ```
313
369
 
314
- `amend_outcome()` takes a mandatory non-blank `reason` plus the same optional fields as `report_outcome()` (`execution_status`, `outcome_type`, `outcome_value`, `outcome_currency`, `metadata`, `reported_by`, `outcome_reason`), and returns the updated job as a dict.
370
+ `amend_outcome()` takes `reason` — the amendment's audit justification, still the first positional argument — plus the same optional fields as `report_outcome()` (`execution_status`, `outcome_type`, `outcome_value`, `outcome_currency`, `metadata`, `reported_by`, `outcome_reason`, `metrics`), and returns the updated job as a dict.
371
+
372
+ - **Detecting a lost update:** `report_outcome()` and `amend_outcome()` record the job's `entityVersion` from the response on the handle (readable as `job.entity_version`). The next `amend_outcome()` on that same handle sends it as `expectedEntityVersion`, so an amendment that would overwrite a change made by another writer in the meantime raises `OutcomeAmendConflictError` instead of silently winning. Pass `expected_entity_version=` to lock against a version you fetched yourself; use a fresh `JobContext.attach()` handle — which has recorded nothing — for the old last-write-wins behavior.
373
+
374
+ ```python
375
+ from revenium_middleware import OutcomeAmendConflictError, get_outcome_history
376
+
377
+ try:
378
+ job.amend_outcome(reason="Chargeback", outcome_value=0.0)
379
+ except OutcomeAmendConflictError as conflict:
380
+ # The conflict reports the version the platform actually holds.
381
+ print(conflict.current_entity_version) # e.g. 9
382
+
383
+ # Look at what the other writer changed, and only re-issue the amendment
384
+ # if it still applies to what is recorded now.
385
+ history = get_outcome_history("sales-lead-8842")
386
+ if still_applies(history[-1]):
387
+ job.amend_outcome(reason="Chargeback, re-checked", outcome_value=0.0,
388
+ expected_entity_version=conflict.current_entity_version)
389
+ ```
390
+
391
+ The handle also records that version, so the retry above works with or without passing `expected_entity_version=` explicitly. `current_entity_version` is `None` when the conflict body carries no version; the version then has to come from a job read (`GET /v2/api/jobs/{agenticJobId}`), which this SDK does not wrap yet, and a retry without it is unlocked (last-write-wins). `get_outcome_history()` rows carry an `amendment_sequence`, not an entity version — history tells you *what* changed, never which version to retry with.
392
+
393
+ Every outcome call replaces the recorded version with the one its response reports, including clearing it when a response carries none, so a completed call never leaves a token behind that the platform has already moved past.
315
394
 
395
+ - **Omitting `reason`:** an API-key caller may leave `reason` out and the platform records an automated correction reason derived from the source. A session caller must supply one; a blank string is rejected client-side either way.
316
396
  - **`reason` vs `outcome_reason`:** `reason` is the amendment's own audit justification (why the record changed); `outcome_reason` is the business explanation of why the job failed or was cancelled. Use `outcome_reason` for failure explanations rather than burying them in `metadata` — it is a first-class field on the outcome and is returned on every `get_outcome_history()` row.
317
397
  - **Clearing `outcome_reason`:** omit the argument to leave the stored value untouched; pass an empty string (`outcome_reason=""`) to clear it.
398
+ - **`metrics`:** both `report_outcome()` and `amend_outcome()` accept a `metrics` argument for recording the measurable facts behind an outcome.
399
+
400
+ ### Recording Metric Facts
401
+
402
+ Beyond the single `outcome_value`, a job can carry the measurable facts its job type declares — `quality_rate` and its siblings — either with the outcome or later, once they are measurable.
403
+
404
+ ```python
405
+ from revenium_middleware import JobContext
406
+
407
+ with JobContext("claim-8842", type="claims_triage") as job:
408
+ ...
409
+ job.report_outcome(
410
+ execution_status="SUCCESS",
411
+ outcome_type="CONVERTED",
412
+ metrics=[
413
+ {"key": "quality_rate", "value": 0.93, "provenance": "MEASURED"},
414
+ {"key": "cases_closed", "value": 12},
415
+ ],
416
+ )
417
+
418
+ # Two days later a human grades a sample of that same job's output.
419
+ handle = JobContext.attach("claim-8842")
420
+ handle.append_outcome_metrics([
421
+ {"key": "quality_rate", "value": 0.87, "provenance": "ATTESTED",
422
+ "reason": "graded sample of 200 cases"},
423
+ ])
424
+ handle.close()
425
+ ```
426
+
427
+ - **Declare the metric first:** a fact only lands if the job type's economics contract declares that key as a `PER_JOB` metric; an undeclared key is rejected with a 400. `quality_rate` is a rate and the platform range-checks it to 0..1.
428
+ - **Entry shape:** `key` and `value` are required; `provenance` (`MEASURED` | `SELF_REPORTED` | `DERIVED` | `ATTESTED`), `recordedBy`, `source`, `reason` and `recordedAt` are optional. Entries are sent exactly as you write them, so the fields you omit take the platform's defaults (`SELF_REPORTED`, the calling principal, `api`) instead of being guessed by the SDK. A missing `key` or `value` — or no entries at all on `append_outcome_metrics()` — raises `ValueError` before any HTTP request, on `JobContext` and `AgenticOutcomeClient` alike.
429
+ - **Append-only:** facts accumulate; the SDK never dedupes or replaces one, because the platform owns fact identity. `metrics=` on `amend_outcome()` appends as part of the amendment.
430
+ - **Not part of outcome history:** `get_outcome_history()` returns the outcome revisions only — appended facts do not appear in those rows.
431
+ - **Retries:** an append is retried only on `429`, which proves the platform rejected the request before recording anything. A `502`/`503`/`504` is raised instead of retried: the facts may already be recorded, and a second append is a second fact, so the decision to resend is yours (check the recorded facts first).
432
+ - **Locking is unaffected:** appending facts does not change the job's `entityVersion`, so the handle keeps the version it recorded and a following `amend_outcome()` still locks against it. (`report_outcome()` and `amend_outcome()` clear the recorded version when their response carries none, because those calls advance it; an append does not.)
433
+ - **Why it matters:** AI Alerts evaluate `QUALITY_RATE` from these facts, so a job whose integration emits none is invisible to those rules.
318
434
 
319
435
  ### Outcome Exceptions
320
436
 
@@ -325,7 +441,7 @@ All outcome exceptions are importable from `revenium_middleware` and share the `
325
441
  | `OutcomeReportingError` | Base class — configuration failures (no API key available, unresolvable `team_id`) | Fix the key / team configuration |
326
442
  | `OutcomeAlreadyReportedError` | Re-reporting a job that already has an outcome (backend 409) | Amend with `amend_outcome()` instead; the exception carries `reported_at` and `amendment_count` |
327
443
  | `OutcomeNotReportedError` | Amending a job that has no outcome yet (backend 422) | Call `report_outcome()` first |
328
- | `OutcomeAmendConflictError` | A concurrent amendment changed the outcome (backend 409, optimistic lock) | Refetch with `get_outcome_history()` and retry — the SDK does not auto-retry |
444
+ | `OutcomeAmendConflictError` | A concurrent amendment changed the outcome (backend 409, optimistic lock) | Re-check the outcome against `get_outcome_history()`, then retry with `expected_entity_version=conflict.current_entity_version` — the SDK does not auto-retry |
329
445
 
330
446
  ### Low-Level Client
331
447
 
@@ -340,10 +456,11 @@ client = AgenticOutcomeClient(settings)
340
456
  client.emit_completion(...) # one per LLM call
341
457
  client.emit_tool_event(...) # one per tool / step
342
458
  client.report_outcome(job_id, {...}) # close the job with a terminal outcome
459
+ client.append_outcome_metrics(job_id, [...]) # append declared per-job facts later
343
460
  client.close()
344
461
  ```
345
462
 
346
- The job is created implicitly by the first metric ingested for `agenticJobId`. Call `client.create_job(job_id)` explicitly if you need to record an agent run before emitting any metrics.
463
+ The job is created implicitly by the first metric ingested for `agenticJobId`. Call `client.create_job(job_id)` explicitly if you need to record an agent run before emitting any metrics; it returns the created job resource merged over the fields you supplied, including the `entityVersion` an outcome amendment sends back as `expectedEntityVersion`.
347
464
 
348
465
  See [`examples/agentic_outcomes/`](examples/agentic_outcomes/) for runnable demos (sales / coding / support) with configurable failure rates and outcome distributions.
349
466
 
@@ -1142,6 +1259,7 @@ Enhanced observability fields for tracking AI operations across environments, re
1142
1259
  | `transaction_name` | `REVENIUM_TRANSACTION_NAME` | Human-friendly operation name | Label operations (e.g., `"Generate Response"`, `"Analyze Sentiment"`) |
1143
1260
  | `retry_number` | `REVENIUM_RETRY_NUMBER` | Retry attempt number (0 = first attempt) | Track retry attempts for failed operations |
1144
1261
  | `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 |
1262
+ | `agent_version` | _(none — per call only)_ | Version of the AI agent that produced the call (max 64 chars) | Compare cost across agent releases; not `agentic_job_version`, which versions the job definition |
1145
1263
  | `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 |
1146
1264
  | `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 |
1147
1265
  | `skill_kind` | `REVENIUM_SKILL_KIND` | Kind of skill invoked — accepted value: `workflow` (omit otherwise) | Distinguish workflow skills in reporting |
@@ -1176,7 +1294,8 @@ response = client.chat.completions.create(
1176
1294
  "trace_name": "Support Chat Session",
1177
1295
  "transaction_name": "Generate Response",
1178
1296
  "parent_transaction_id": "parent-txn-123",
1179
- "ticket_id": "JIRA-123"
1297
+ "ticket_id": "JIRA-123",
1298
+ "agent_version": "1.4.2"
1180
1299
  }
1181
1300
  )
1182
1301
  ```
@@ -1593,7 +1712,8 @@ print(get_buffer_stats())
1593
1712
  | `REVENIUM_AGENTIC_JOB_NAME` | - | Human-readable agentic job name |
1594
1713
  | `REVENIUM_AGENTIC_JOB_TYPE` | - | Agentic job type category |
1595
1714
  | `REVENIUM_AGENTIC_JOB_VERSION` | - | Agentic job version |
1596
- | `REVENIUM_OUTCOME_API_KEY` | - | Write-scope key (`rev_sk_`) for the agentic outcomes API (report/amend/history); falls back to `REVENIUM_METERING_API_KEY` |
1715
+ | `REVENIUM_WRITE_API_KEY` | - | Primary write-scope key (`rev_sk_`) for the agentic outcomes API (report/amend/history); falls back to `REVENIUM_OUTCOME_API_KEY` (deprecated), then `REVENIUM_METERING_API_KEY` |
1716
+ | `REVENIUM_OUTCOME_API_KEY` | - | Deprecated fallback name for the write-scope key; used only when `REVENIUM_WRITE_API_KEY` is unset |
1597
1717
  | `REVENIUM_PROFITSTREAM_BASE_URL` | `https://api.revenium.io` | Agentic outcomes API base URL |
1598
1718
  | `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
1599
1719
  | `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
@@ -167,7 +167,63 @@ when the provider's SDK is installed.
167
167
 
168
168
  Emit per-agent terminal outcomes (`CONVERTED`, `DEFLECTED`, `ESCALATED`) alongside completion and tool-event records, so dashboards show business value next to AI cost.
169
169
 
170
- > **You need a write-scope key (`rev_sk_`) to use the agentic outcomes API.** Metering keys (`rev_mk_`) can only meter completions and tool events — they cannot report, amend, or read job outcomes, and the SDK rejects them client-side before any HTTP request is made. Key resolution: explicit `api_key=` > `REVENIUM_OUTCOME_API_KEY` > `REVENIUM_METERING_API_KEY`.
170
+ > **You need a write-scope key (`rev_sk_`) to use the agentic outcomes API.** Metering keys (`rev_mk_`) can only meter completions and tool events — they cannot report, amend, or read job outcomes, and the SDK rejects them client-side before any HTTP request is made. Key resolution: explicit `api_key=` > `REVENIUM_WRITE_API_KEY` > `REVENIUM_OUTCOME_API_KEY` (deprecated fallback) > `REVENIUM_METERING_API_KEY`.
171
+
172
+ ### Job-Type Economics and Outcome Facts
173
+
174
+ Keep a metering key for AI telemetry and a separate write key for outcomes and
175
+ job-type configuration. A registered `valuePerUnit` rule takes precedence over
176
+ an outcome's `outcome_value`; the backend never sums the two value sources.
177
+
178
+ ```python
179
+ from revenium_middleware import (
180
+ Baseline, JobTypeEconomics, PeriodFactEntry, create_baseline,
181
+ report_period_facts, upsert_job_type_economics,
182
+ )
183
+
184
+ upsert_job_type_economics("claim", JobTypeEconomics(
185
+ unit_metric_key="completed_claims", unit_label="claim",
186
+ metrics=[{
187
+ "key": "completed_claims", "type": "COUNT",
188
+ "direction": "HIGHER_IS_BETTER", "aggregation": "SUM",
189
+ "resolution": "PER_JOB",
190
+ }],
191
+ dimensions=[{"key": "region", "allowedValues": ["us", "ca"]}],
192
+ monetization={
193
+ "metricKey": "completed_claims", "valuePerUnit": 4.25,
194
+ "currency": "USD", "category": "COST_AVOIDED", "basis": "REALIZED",
195
+ },
196
+ ))
197
+ create_baseline("claim", Baseline(
198
+ effective_from="2026-08-01T00:00:00Z", cost_per_unit=4.25, currency="USD",
199
+ ))
200
+ report_period_facts("claim", [PeriodFactEntry(
201
+ period_start="2026-08-01T00:00:00Z", period_end="2026-09-01T00:00:00Z",
202
+ dimension_key="region", dimension_value="us",
203
+ key="completed_claims", value=1280,
204
+ )])
205
+ ```
206
+
207
+ `effective_from` is the only required field on a baseline; every other field
208
+ is optional, and a baseline without it is rejected. A job type must be
209
+ declared with `upsert_job_type_economics` before it accepts baselines or
210
+ facts, and `report_period_facts` accepts only metrics declared with
211
+ `"resolution": "PERIOD"`.
212
+
213
+ Job economics currency values must be USD. Baselines and period facts use
214
+ server-supplied attribution when their provenance,
215
+ reporter, and source fields are omitted. Set those fields only when you need an
216
+ explicit override. Economics metric directions are
217
+ `HIGHER_IS_BETTER` or `LOWER_IS_BETTER`; monetization categories are
218
+ `REVENUE`, `COST_AVOIDED`, `TIME_SAVED`, and `LEADING_VALUE`, with a
219
+ `REALIZED` or `EXPECTED` basis.
220
+
221
+ Use `CUSTOMER_DECLARED` or `MEASURED` for a baseline override. Use `MEASURED`,
222
+ `SELF_REPORTED`, or `DERIVED` for a period fact override.
223
+
224
+ Facts are append-only and keyed on the period, dimension and metric key
225
+ together. Re-appending that tuple supersedes the active fact, and the server
226
+ requires `reason=` on the entry when it does.
171
227
 
172
228
  ### JobContext
173
229
 
@@ -214,10 +270,70 @@ history = get_outcome_history("sales-lead-8842")
214
270
  # List[JobOutcomeAmendment], ordered by amendment_sequence (1 = the initial report)
215
271
  ```
216
272
 
217
- `amend_outcome()` takes a mandatory non-blank `reason` plus the same optional fields as `report_outcome()` (`execution_status`, `outcome_type`, `outcome_value`, `outcome_currency`, `metadata`, `reported_by`, `outcome_reason`), and returns the updated job as a dict.
273
+ `amend_outcome()` takes `reason` — the amendment's audit justification, still the first positional argument — plus the same optional fields as `report_outcome()` (`execution_status`, `outcome_type`, `outcome_value`, `outcome_currency`, `metadata`, `reported_by`, `outcome_reason`, `metrics`), and returns the updated job as a dict.
274
+
275
+ - **Detecting a lost update:** `report_outcome()` and `amend_outcome()` record the job's `entityVersion` from the response on the handle (readable as `job.entity_version`). The next `amend_outcome()` on that same handle sends it as `expectedEntityVersion`, so an amendment that would overwrite a change made by another writer in the meantime raises `OutcomeAmendConflictError` instead of silently winning. Pass `expected_entity_version=` to lock against a version you fetched yourself; use a fresh `JobContext.attach()` handle — which has recorded nothing — for the old last-write-wins behavior.
276
+
277
+ ```python
278
+ from revenium_middleware import OutcomeAmendConflictError, get_outcome_history
279
+
280
+ try:
281
+ job.amend_outcome(reason="Chargeback", outcome_value=0.0)
282
+ except OutcomeAmendConflictError as conflict:
283
+ # The conflict reports the version the platform actually holds.
284
+ print(conflict.current_entity_version) # e.g. 9
285
+
286
+ # Look at what the other writer changed, and only re-issue the amendment
287
+ # if it still applies to what is recorded now.
288
+ history = get_outcome_history("sales-lead-8842")
289
+ if still_applies(history[-1]):
290
+ job.amend_outcome(reason="Chargeback, re-checked", outcome_value=0.0,
291
+ expected_entity_version=conflict.current_entity_version)
292
+ ```
293
+
294
+ The handle also records that version, so the retry above works with or without passing `expected_entity_version=` explicitly. `current_entity_version` is `None` when the conflict body carries no version; the version then has to come from a job read (`GET /v2/api/jobs/{agenticJobId}`), which this SDK does not wrap yet, and a retry without it is unlocked (last-write-wins). `get_outcome_history()` rows carry an `amendment_sequence`, not an entity version — history tells you *what* changed, never which version to retry with.
295
+
296
+ Every outcome call replaces the recorded version with the one its response reports, including clearing it when a response carries none, so a completed call never leaves a token behind that the platform has already moved past.
218
297
 
298
+ - **Omitting `reason`:** an API-key caller may leave `reason` out and the platform records an automated correction reason derived from the source. A session caller must supply one; a blank string is rejected client-side either way.
219
299
  - **`reason` vs `outcome_reason`:** `reason` is the amendment's own audit justification (why the record changed); `outcome_reason` is the business explanation of why the job failed or was cancelled. Use `outcome_reason` for failure explanations rather than burying them in `metadata` — it is a first-class field on the outcome and is returned on every `get_outcome_history()` row.
220
300
  - **Clearing `outcome_reason`:** omit the argument to leave the stored value untouched; pass an empty string (`outcome_reason=""`) to clear it.
301
+ - **`metrics`:** both `report_outcome()` and `amend_outcome()` accept a `metrics` argument for recording the measurable facts behind an outcome.
302
+
303
+ ### Recording Metric Facts
304
+
305
+ Beyond the single `outcome_value`, a job can carry the measurable facts its job type declares — `quality_rate` and its siblings — either with the outcome or later, once they are measurable.
306
+
307
+ ```python
308
+ from revenium_middleware import JobContext
309
+
310
+ with JobContext("claim-8842", type="claims_triage") as job:
311
+ ...
312
+ job.report_outcome(
313
+ execution_status="SUCCESS",
314
+ outcome_type="CONVERTED",
315
+ metrics=[
316
+ {"key": "quality_rate", "value": 0.93, "provenance": "MEASURED"},
317
+ {"key": "cases_closed", "value": 12},
318
+ ],
319
+ )
320
+
321
+ # Two days later a human grades a sample of that same job's output.
322
+ handle = JobContext.attach("claim-8842")
323
+ handle.append_outcome_metrics([
324
+ {"key": "quality_rate", "value": 0.87, "provenance": "ATTESTED",
325
+ "reason": "graded sample of 200 cases"},
326
+ ])
327
+ handle.close()
328
+ ```
329
+
330
+ - **Declare the metric first:** a fact only lands if the job type's economics contract declares that key as a `PER_JOB` metric; an undeclared key is rejected with a 400. `quality_rate` is a rate and the platform range-checks it to 0..1.
331
+ - **Entry shape:** `key` and `value` are required; `provenance` (`MEASURED` | `SELF_REPORTED` | `DERIVED` | `ATTESTED`), `recordedBy`, `source`, `reason` and `recordedAt` are optional. Entries are sent exactly as you write them, so the fields you omit take the platform's defaults (`SELF_REPORTED`, the calling principal, `api`) instead of being guessed by the SDK. A missing `key` or `value` — or no entries at all on `append_outcome_metrics()` — raises `ValueError` before any HTTP request, on `JobContext` and `AgenticOutcomeClient` alike.
332
+ - **Append-only:** facts accumulate; the SDK never dedupes or replaces one, because the platform owns fact identity. `metrics=` on `amend_outcome()` appends as part of the amendment.
333
+ - **Not part of outcome history:** `get_outcome_history()` returns the outcome revisions only — appended facts do not appear in those rows.
334
+ - **Retries:** an append is retried only on `429`, which proves the platform rejected the request before recording anything. A `502`/`503`/`504` is raised instead of retried: the facts may already be recorded, and a second append is a second fact, so the decision to resend is yours (check the recorded facts first).
335
+ - **Locking is unaffected:** appending facts does not change the job's `entityVersion`, so the handle keeps the version it recorded and a following `amend_outcome()` still locks against it. (`report_outcome()` and `amend_outcome()` clear the recorded version when their response carries none, because those calls advance it; an append does not.)
336
+ - **Why it matters:** AI Alerts evaluate `QUALITY_RATE` from these facts, so a job whose integration emits none is invisible to those rules.
221
337
 
222
338
  ### Outcome Exceptions
223
339
 
@@ -228,7 +344,7 @@ All outcome exceptions are importable from `revenium_middleware` and share the `
228
344
  | `OutcomeReportingError` | Base class — configuration failures (no API key available, unresolvable `team_id`) | Fix the key / team configuration |
229
345
  | `OutcomeAlreadyReportedError` | Re-reporting a job that already has an outcome (backend 409) | Amend with `amend_outcome()` instead; the exception carries `reported_at` and `amendment_count` |
230
346
  | `OutcomeNotReportedError` | Amending a job that has no outcome yet (backend 422) | Call `report_outcome()` first |
231
- | `OutcomeAmendConflictError` | A concurrent amendment changed the outcome (backend 409, optimistic lock) | Refetch with `get_outcome_history()` and retry — the SDK does not auto-retry |
347
+ | `OutcomeAmendConflictError` | A concurrent amendment changed the outcome (backend 409, optimistic lock) | Re-check the outcome against `get_outcome_history()`, then retry with `expected_entity_version=conflict.current_entity_version` — the SDK does not auto-retry |
232
348
 
233
349
  ### Low-Level Client
234
350
 
@@ -243,10 +359,11 @@ client = AgenticOutcomeClient(settings)
243
359
  client.emit_completion(...) # one per LLM call
244
360
  client.emit_tool_event(...) # one per tool / step
245
361
  client.report_outcome(job_id, {...}) # close the job with a terminal outcome
362
+ client.append_outcome_metrics(job_id, [...]) # append declared per-job facts later
246
363
  client.close()
247
364
  ```
248
365
 
249
- The job is created implicitly by the first metric ingested for `agenticJobId`. Call `client.create_job(job_id)` explicitly if you need to record an agent run before emitting any metrics.
366
+ The job is created implicitly by the first metric ingested for `agenticJobId`. Call `client.create_job(job_id)` explicitly if you need to record an agent run before emitting any metrics; it returns the created job resource merged over the fields you supplied, including the `entityVersion` an outcome amendment sends back as `expectedEntityVersion`.
250
367
 
251
368
  See [`examples/agentic_outcomes/`](examples/agentic_outcomes/) for runnable demos (sales / coding / support) with configurable failure rates and outcome distributions.
252
369
 
@@ -1045,6 +1162,7 @@ Enhanced observability fields for tracking AI operations across environments, re
1045
1162
  | `transaction_name` | `REVENIUM_TRANSACTION_NAME` | Human-friendly operation name | Label operations (e.g., `"Generate Response"`, `"Analyze Sentiment"`) |
1046
1163
  | `retry_number` | `REVENIUM_RETRY_NUMBER` | Retry attempt number (0 = first attempt) | Track retry attempts for failed operations |
1047
1164
  | `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 |
1165
+ | `agent_version` | _(none — per call only)_ | Version of the AI agent that produced the call (max 64 chars) | Compare cost across agent releases; not `agentic_job_version`, which versions the job definition |
1048
1166
  | `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 |
1049
1167
  | `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 |
1050
1168
  | `skill_kind` | `REVENIUM_SKILL_KIND` | Kind of skill invoked — accepted value: `workflow` (omit otherwise) | Distinguish workflow skills in reporting |
@@ -1079,7 +1197,8 @@ response = client.chat.completions.create(
1079
1197
  "trace_name": "Support Chat Session",
1080
1198
  "transaction_name": "Generate Response",
1081
1199
  "parent_transaction_id": "parent-txn-123",
1082
- "ticket_id": "JIRA-123"
1200
+ "ticket_id": "JIRA-123",
1201
+ "agent_version": "1.4.2"
1083
1202
  }
1084
1203
  )
1085
1204
  ```
@@ -1496,7 +1615,8 @@ print(get_buffer_stats())
1496
1615
  | `REVENIUM_AGENTIC_JOB_NAME` | - | Human-readable agentic job name |
1497
1616
  | `REVENIUM_AGENTIC_JOB_TYPE` | - | Agentic job type category |
1498
1617
  | `REVENIUM_AGENTIC_JOB_VERSION` | - | Agentic job version |
1499
- | `REVENIUM_OUTCOME_API_KEY` | - | Write-scope key (`rev_sk_`) for the agentic outcomes API (report/amend/history); falls back to `REVENIUM_METERING_API_KEY` |
1618
+ | `REVENIUM_WRITE_API_KEY` | - | Primary write-scope key (`rev_sk_`) for the agentic outcomes API (report/amend/history); falls back to `REVENIUM_OUTCOME_API_KEY` (deprecated), then `REVENIUM_METERING_API_KEY` |
1619
+ | `REVENIUM_OUTCOME_API_KEY` | - | Deprecated fallback name for the write-scope key; used only when `REVENIUM_WRITE_API_KEY` is unset |
1500
1620
  | `REVENIUM_PROFITSTREAM_BASE_URL` | `https://api.revenium.io` | Agentic outcomes API base URL |
1501
1621
  | `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
1502
1622
  | `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "revenium-python-sdk"
7
- version = "0.7.0"
7
+ version = "0.8.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"
@@ -179,6 +179,13 @@ from .agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings # no
179
179
  # Public job-context surface (BACK-777)
180
180
  from .job_context import JobContext # noqa: E402
181
181
  from .job_history import JobOutcomeAmendment, get_outcome_history # noqa: E402
182
+ from .job_type_economics import ( # noqa: E402
183
+ Baseline, BaselineProvenance, JobTypeEconomics, JobTypeMetricDirection,
184
+ JobTypeMonetizationBasis, JobTypeMonetizationCategory,
185
+ OutcomeMetricProvenance, PeriodFactEntry,
186
+ create_baseline, get_job_type_economics, list_baselines,
187
+ report_period_facts, upsert_job_type_economics,
188
+ )
182
189
  from ._core.exceptions import ( # noqa: E402
183
190
  OutcomeAlreadyReportedError,
184
191
  OutcomeAmendConflictError,
@@ -235,4 +242,17 @@ __all__ = [
235
242
  "JobOutcomeAmendment",
236
243
  "OutcomeNotReportedError",
237
244
  "OutcomeAmendConflictError",
245
+ "PeriodFactEntry",
246
+ "Baseline",
247
+ "BaselineProvenance",
248
+ "JobTypeEconomics",
249
+ "JobTypeMetricDirection",
250
+ "JobTypeMonetizationBasis",
251
+ "JobTypeMonetizationCategory",
252
+ "OutcomeMetricProvenance",
253
+ "report_period_facts",
254
+ "get_job_type_economics",
255
+ "upsert_job_type_economics",
256
+ "create_baseline",
257
+ "list_baselines",
238
258
  ]
@@ -8,6 +8,7 @@ this via class inheritance and re-export symbols for backward compatibility.
8
8
 
9
9
  import logging
10
10
  import os
11
+ import warnings
11
12
  from typing import Set, Optional
12
13
 
13
14
  logger = logging.getLogger(__name__)
@@ -70,6 +71,7 @@ class Config:
70
71
  ENV_REVENIUM_SKILL_NAME: str = "REVENIUM_SKILL_NAME"
71
72
  ENV_REVENIUM_SKILL_PLUGIN_NAME: str = "REVENIUM_SKILL_PLUGIN_NAME"
72
73
  ENV_REVENIUM_SKILL_SOURCE: str = "REVENIUM_SKILL_SOURCE"
74
+ ENV_REVENIUM_WRITE_API_KEY: str = "REVENIUM_WRITE_API_KEY"
73
75
  ENV_REVENIUM_OUTCOME_API_KEY: str = "REVENIUM_OUTCOME_API_KEY"
74
76
  ENV_REVENIUM_PROFITSTREAM_BASE_URL: str = "REVENIUM_PROFITSTREAM_BASE_URL"
75
77
 
@@ -158,6 +160,27 @@ def get_base_url() -> str:
158
160
  return get_config_value(Config.ENV_REVENIUM_BASE_URL, Config.DEFAULT_BASE_URL)
159
161
 
160
162
 
163
+ def resolve_write_api_key(api_key: Optional[str] = None) -> Optional[str]:
164
+ """Resolve the write-scope key from the supported precedence chain."""
165
+ if api_key:
166
+ return api_key
167
+
168
+ write_key = os.getenv(Config.ENV_REVENIUM_WRITE_API_KEY)
169
+ if write_key:
170
+ return write_key
171
+
172
+ outcome_key = os.getenv(Config.ENV_REVENIUM_OUTCOME_API_KEY)
173
+ if outcome_key:
174
+ warnings.warn(
175
+ "REVENIUM_OUTCOME_API_KEY is deprecated; set REVENIUM_WRITE_API_KEY instead.",
176
+ DeprecationWarning,
177
+ stacklevel=2,
178
+ )
179
+ return outcome_key
180
+
181
+ return os.getenv(Config.ENV_REVENIUM_API_KEY)
182
+
183
+
161
184
  def validate_api_key(api_key: str) -> None:
162
185
  """Raise ``ValueError`` if ``api_key`` does not start with a recognized Revenium prefix.
163
186
 
@@ -100,11 +100,26 @@ def clear_injected_metadata() -> None:
100
100
  _injected_metadata_context.set(None)
101
101
 
102
102
 
103
+ def _canonical_metadata_key(key: Any) -> Any:
104
+ """Fold a metadata key to a spelling-independent form.
105
+
106
+ Every aliased metadata field in this SDK is accepted as both snake_case
107
+ and camelCase (``agent_version`` / ``agentVersion``, ``ticket_id`` /
108
+ ``ticketId``), so both spellings fold to the same canonical string.
109
+ Non-string keys are returned unchanged and can only ever collide with
110
+ themselves.
111
+ """
112
+ if not isinstance(key, str):
113
+ return key
114
+ return key.replace("_", "").lower()
115
+
116
+
103
117
  def merge_metadata(api_metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
104
118
  """
105
119
  Merge injected metadata with API-level metadata.
106
120
 
107
- API-level metadata takes precedence over injected metadata.
121
+ API-level metadata takes precedence over injected metadata, including
122
+ when the two sides spell the same field differently.
108
123
 
109
124
  Args:
110
125
  api_metadata: Metadata passed directly to the API call
@@ -115,6 +130,20 @@ def merge_metadata(api_metadata: Optional[Dict[str, Any]] = None) -> Dict[str, A
115
130
  injected = get_injected_metadata() or {}
116
131
  api = api_metadata or {}
117
132
 
133
+ if injected and api:
134
+ # Merging by literal key alone keeps a scoped ``agent_version``
135
+ # alongside a direct ``agentVersion``. The alias precedence applied
136
+ # downstream then picks whichever spelling it looks for first, which
137
+ # can resolve the scoped value and invert the precedence documented
138
+ # above. Drop scoped keys the direct call already supplies under any
139
+ # spelling, so the direct value is the only one left to resolve.
140
+ api_keys = {_canonical_metadata_key(key) for key in api}
141
+ injected = {
142
+ key: value
143
+ for key, value in injected.items()
144
+ if _canonical_metadata_key(key) not in api_keys
145
+ }
146
+
118
147
  # Start with injected metadata, then override with API-level metadata
119
148
  merged = {**injected, **api}
120
149
  return merged