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.
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/PKG-INFO +127 -7
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/README.md +126 -6
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/pyproject.toml +1 -1
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/__init__.py +20 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/config.py +23 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/context.py +30 -1
- revenium_python_sdk-0.8.0/revenium_middleware/_core/enforcement.py +1348 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/exceptions.py +22 -5
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/fields.py +29 -0
- revenium_python_sdk-0.8.0/revenium_middleware/_core/outcomes.py +728 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/trace_fields.py +82 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/ai.py +32 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_audio_params.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_completion_params.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_image_params.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/ai_create_video_params.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/agentic_outcomes.py +102 -7
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/bedrock_transport.py +1 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/middleware.py +8 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/trace_fields.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/_metering.py +10 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/trace_fields.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/trace_fields.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/utils.py +12 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/job_context.py +272 -37
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/job_history.py +8 -10
- revenium_python_sdk-0.8.0/revenium_middleware/job_type_economics.py +214 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/middleware.py +4 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/trace_fields.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/ollama/middleware.py +5 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/ollama/trace_fields.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/middleware.py +13 -3
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/trace_fields.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/middleware.py +4 -1
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/perplexity_sdk.py +4 -1
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/trace_fields.py +3 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/PKG-INFO +127 -7
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/SOURCES.txt +1 -0
- revenium_python_sdk-0.7.0/revenium_middleware/_core/enforcement.py +0 -817
- revenium_python_sdk-0.7.0/revenium_middleware/_core/outcomes.py +0 -424
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/LICENSE +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/cache_tokens.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/decorators.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/load_diagnostics.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/log_sanitize.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering_buffer.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering_status.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/metering_submission.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/patch_registry.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/subscriber.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/LICENSE +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_base_client.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_client.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_compat.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_constants.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_exceptions.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_files.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_models.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_qs.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_resource.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_response.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_streaming.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_types.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_logs.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_proxy.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_reflection.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_resources_proxy.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_streams.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_sync.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_transform.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_typing.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_utils/_utils.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/_version.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/context.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/decorator.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/py.typed +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/apis.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/resources/events.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/api_meter_request_params.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/api_meter_response_params.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/event_create_params.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_metering/types/metering_response_resource.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/bedrock_adapter.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/config.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/provider.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/anthropic/stream_create.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/config.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/fal/middleware.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/exceptions.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/protocols.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/common/types.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/config.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/google_ai/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/google_ai/middleware.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/google_ai/provider.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/vertex_ai/middleware.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/_metadata.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/anthropic_driver.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/litellm_driver.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/ollama_driver.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/openai_driver.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/openai_embedding_driver.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/griptape/universal_driver.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/config.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/context.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/decorators.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/hooks.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/client/validation.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/proxy/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/litellm/proxy/middleware.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/ollama/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/azure_config.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/config.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/exceptions.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/openai/provider.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/perplexity/provider.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/webhooks/__init__.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/webhooks/_verify.py +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/requires.txt +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
- {revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/setup.cfg +0 -0
- {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.
|
|
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
|
|
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) |
|
|
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
|
-
| `
|
|
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
|
|
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) |
|
|
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
|
-
| `
|
|
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
|
+
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
|
|
{revenium_python_sdk-0.7.0 → revenium_python_sdk-0.8.0}/revenium_middleware/_core/context.py
RENAMED
|
@@ -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
|