revenium-python-sdk 0.1.5__tar.gz → 0.1.7__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 (92) hide show
  1. {revenium_python_sdk-0.1.5/revenium_python_sdk.egg-info → revenium_python_sdk-0.1.7}/PKG-INFO +112 -77
  2. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/README.md +111 -76
  3. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/pyproject.toml +1 -1
  4. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/__init__.py +6 -0
  5. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/__init__.py +8 -0
  6. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/config.py +2 -41
  7. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/context.py +66 -1
  8. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/fields.py +23 -5
  9. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/metering.py +13 -4
  10. revenium_python_sdk-0.1.7/revenium_middleware/_core/metering_submission.py +68 -0
  11. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/agentic_outcomes.py +3 -0
  12. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/anthropic/bedrock_adapter.py +35 -34
  13. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/anthropic/config.py +0 -3
  14. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/anthropic/middleware.py +184 -258
  15. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/fal/_metering.py +42 -37
  16. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/common/__init__.py +0 -14
  17. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/common/trace_fields.py +3 -11
  18. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/common/utils.py +4 -69
  19. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/config.py +0 -1
  20. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/config.py +0 -16
  21. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/middleware.py +2 -21
  22. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/proxy/middleware.py +3 -2
  23. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/ollama/middleware.py +3 -2
  24. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/config.py +0 -3
  25. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/middleware.py +2 -27
  26. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/perplexity/middleware.py +2 -1
  27. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/perplexity/perplexity_sdk.py +2 -1
  28. revenium_python_sdk-0.1.7/revenium_middleware/webhooks/__init__.py +4 -0
  29. revenium_python_sdk-0.1.7/revenium_middleware/webhooks/_verify.py +74 -0
  30. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7/revenium_python_sdk.egg-info}/PKG-INFO +112 -77
  31. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_python_sdk.egg-info/SOURCES.txt +3 -4
  32. revenium_python_sdk-0.1.5/revenium_middleware/anthropic/summary_printer.py +0 -286
  33. revenium_python_sdk-0.1.5/revenium_middleware/google/common/summary_printer.py +0 -271
  34. revenium_python_sdk-0.1.5/revenium_middleware/litellm/client/summary_printer.py +0 -314
  35. revenium_python_sdk-0.1.5/revenium_middleware/openai/summary_printer.py +0 -292
  36. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/LICENSE +0 -0
  37. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/decorators.py +0 -0
  38. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/enforcement.py +0 -0
  39. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/exceptions.py +0 -0
  40. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/patch_registry.py +0 -0
  41. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/prompt_extraction.py +0 -0
  42. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/subscriber.py +0 -0
  43. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/_core/trace_fields.py +0 -0
  44. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/anthropic/__init__.py +0 -0
  45. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
  46. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/anthropic/provider.py +0 -0
  47. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/anthropic/trace_fields.py +0 -0
  48. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/fal/__init__.py +0 -0
  49. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/fal/config.py +0 -0
  50. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/fal/middleware.py +0 -0
  51. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/fal/trace_fields.py +0 -0
  52. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/__init__.py +0 -0
  53. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/common/exceptions.py +0 -0
  54. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/common/protocols.py +0 -0
  55. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/common/types.py +0 -0
  56. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/google_ai/__init__.py +0 -0
  57. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/google_ai/middleware.py +0 -0
  58. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/google_ai/provider.py +0 -0
  59. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/prompt_extractor.py +0 -0
  60. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
  61. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/vertex_ai/middleware.py +0 -0
  62. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/google/vertex_ai/provider.py +0 -0
  63. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/__init__.py +0 -0
  64. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/__init__.py +0 -0
  65. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/context.py +0 -0
  66. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/decorators.py +0 -0
  67. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/hooks.py +0 -0
  68. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
  69. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
  70. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/trace_fields.py +0 -0
  71. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/client/validation.py +0 -0
  72. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/litellm/proxy/__init__.py +0 -0
  73. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/ollama/__init__.py +0 -0
  74. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/ollama/trace_fields.py +0 -0
  75. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/__init__.py +0 -0
  76. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/azure_config.py +0 -0
  77. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/azure_model_resolver.py +0 -0
  78. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/exceptions.py +0 -0
  79. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/langchain/__init__.py +0 -0
  80. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/langchain/_utils.py +0 -0
  81. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
  82. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/prompt_extractor.py +0 -0
  83. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/provider.py +0 -0
  84. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/openai/trace_fields.py +0 -0
  85. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/perplexity/__init__.py +0 -0
  86. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/perplexity/provider.py +0 -0
  87. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_middleware/perplexity/trace_fields.py +0 -0
  88. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
  89. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_python_sdk.egg-info/requires.txt +0 -0
  90. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/revenium_python_sdk.egg-info/top_level.txt +0 -0
  91. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/setup.cfg +0 -0
  92. {revenium_python_sdk-0.1.5 → revenium_python_sdk-0.1.7}/tests/test_metering.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revenium-python-sdk
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -194,7 +194,7 @@ import revenium_middleware_openai # Auto-initializes on import
194
194
 
195
195
  client = openai.OpenAI()
196
196
  response = client.chat.completions.create(
197
- model="gpt-5.5",
197
+ model="gpt-4o-mini",
198
198
  messages=[{"role": "user", "content": "Hello!"}]
199
199
  )
200
200
  print(response.choices[0].message.content)
@@ -227,6 +227,98 @@ See [`examples/agentic_outcomes/`](examples/agentic_outcomes/) for runnable demo
227
227
 
228
228
  ---
229
229
 
230
+ ## Idempotency
231
+
232
+ Every metering POST from the provider middleware automatically includes an `Idempotency-Key` header. If the Revenium API receives the same key with the same body within 24 hours, it returns the cached response instead of double-billing — making metering submissions safe to retry.
233
+
234
+ ### Default
235
+
236
+ A fresh UUID v4 is generated automatically for every metering call. No action required.
237
+
238
+ ### Override
239
+
240
+ Use the `idempotency_key` context manager to tie metering to a business-level identifier so the same logical operation never double-meters across retries:
241
+
242
+ ```python
243
+ from revenium_middleware import idempotency_key
244
+
245
+ with idempotency_key(f"order-{order_id}"):
246
+ response = openai.chat.completions.create(...)
247
+ ```
248
+
249
+ The context manager is backed by `contextvars`, so it scopes correctly across threads and asyncio tasks.
250
+
251
+ ### Backend behavior
252
+
253
+ | Scenario | Backend response |
254
+ | -- | -- |
255
+ | First call with key K and body B | Executes, caches for 24h |
256
+ | Retry with same K and same B | Returns cached response (no double-bill) |
257
+ | Same K with different B | `409 idempotency_key_mismatch` |
258
+ | Concurrent in-flight with same K | `409 idempotency_key_in_progress` + `Retry-After: 1` |
259
+ | Malformed key | `400 invalid_idempotency_key` |
260
+
261
+ See [docs.revenium.io/integrations/idempotency](https://docs.revenium.io/integrations/idempotency) for full backend semantics.
262
+
263
+ ### Key format
264
+
265
+ `Idempotency-Key` must be 1–255 printable ASCII characters. UUID v4 is the recommended format and what the SDK generates by default.
266
+
267
+ ---
268
+
269
+ ## Webhook Signature Verification
270
+
271
+ Revenium signs every outbound webhook with HMAC-SHA256 when a signing secret is configured. The SDK ships a verification helper so your handler can validate signatures without writing crypto.
272
+
273
+ Two headers arrive on every signed delivery:
274
+
275
+ | Header | Value |
276
+ | -- | -- |
277
+ | `X-Revenium-Signature-256` | `sha256=<hex>`. During a 24h rotation overlap: `sha256=A, sha256=B`. |
278
+ | `X-Revenium-Webhook-Timestamp` | Unix seconds at signing time. |
279
+
280
+ ### FastAPI example
281
+
282
+ ```python
283
+ import os
284
+
285
+ from fastapi import FastAPI, Header, HTTPException, Request
286
+
287
+ from revenium_middleware.webhooks import verify_signature
288
+
289
+ app = FastAPI()
290
+ SIGNING_SECRETS = [os.environ["REVENIUM_WEBHOOK_SECRET"]]
291
+
292
+
293
+ @app.post("/webhooks/revenium")
294
+ async def receive(
295
+ request: Request,
296
+ x_revenium_signature_256: str = Header(...),
297
+ x_revenium_webhook_timestamp: str = Header(...),
298
+ ):
299
+ body = await request.body()
300
+ if not verify_signature(
301
+ payload=body,
302
+ signature_header=x_revenium_signature_256,
303
+ timestamp_header=x_revenium_webhook_timestamp,
304
+ secrets=SIGNING_SECRETS,
305
+ ):
306
+ raise HTTPException(status_code=401, detail="Invalid signature")
307
+
308
+ # ... process the event
309
+ return {"ok": True}
310
+ ```
311
+
312
+ ### Secret rotation
313
+
314
+ When you rotate a signing secret in the Revenium dashboard with the default 24-hour overlap, both the old and new secrets are active simultaneously and every webhook is signed with both. Supply both values in `SIGNING_SECRETS` during the overlap window; remove the old one once it expires.
315
+
316
+ ### Webhooks without a signing secret
317
+
318
+ Webhook deliveries without a configured signing secret arrive without HMAC headers. If your endpoint receives both signed and unsigned traffic, branch on header presence: treat missing headers as legacy unsigned mode and missing-signature-on-signed-only endpoints as an authentication failure.
319
+
320
+ ---
321
+
230
322
  ## Provider Usage Guides
231
323
 
232
324
  ### OpenAI
@@ -244,7 +336,7 @@ client = openai.OpenAI()
244
336
 
245
337
  # Basic chat completion
246
338
  response = client.chat.completions.create(
247
- model="gpt-5.5",
339
+ model="gpt-4o-mini",
248
340
  messages=[{"role": "user", "content": "Hello!"}],
249
341
  usage_metadata={
250
342
  "organizationName": "AcmeCorp",
@@ -256,7 +348,7 @@ response = client.chat.completions.create(
256
348
 
257
349
  # Streaming
258
350
  stream = client.chat.completions.create(
259
- model="gpt-5.5",
351
+ model="gpt-4o-mini",
260
352
  messages=[{"role": "user", "content": "Tell me a story"}],
261
353
  stream=True
262
354
  )
@@ -547,7 +639,7 @@ litellm.api_base = os.getenv("LITELLM_PROXY_URL")
547
639
  litellm.api_key = os.getenv("LITELLM_API_KEY")
548
640
 
549
641
  response = litellm.completion(
550
- model="gpt-5.5",
642
+ model="gpt-4o-mini",
551
643
  messages=[{"role": "user", "content": "Hello!"}],
552
644
  usage_metadata={
553
645
  "organizationName": "AcmeCorp",
@@ -705,7 +797,7 @@ handler = ReveniumCallbackHandler(
705
797
  agent_name="support_agent"
706
798
  )
707
799
 
708
- llm = ChatOpenAI(model="gpt-5.5", callbacks=[handler])
800
+ llm = ChatOpenAI(model="gpt-4o-mini", callbacks=[handler])
709
801
  response = llm.invoke("Hello!")
710
802
  ```
711
803
 
@@ -744,7 +836,7 @@ result = agent.invoke(
744
836
  from revenium_middleware_langchain import AsyncReveniumCallbackHandler
745
837
 
746
838
  handler = AsyncReveniumCallbackHandler(trace_id="async-session")
747
- llm = ChatOpenAI(model="gpt-5.5", callbacks=[handler])
839
+ llm = ChatOpenAI(model="gpt-4o-mini", callbacks=[handler])
748
840
  response = await llm.ainvoke("Hello!")
749
841
  ```
750
842
 
@@ -790,7 +882,7 @@ Add business context to any API call by passing a `usage_metadata` dictionary. A
790
882
 
791
883
  ```python
792
884
  response = client.chat.completions.create(
793
- model="gpt-5.5",
885
+ model="gpt-4o-mini",
794
886
  messages=[{"role": "user", "content": "Hello!"}],
795
887
  usage_metadata={
796
888
  "trace_id": "conv-28a7e9d4",
@@ -812,7 +904,7 @@ response = client.chat.completions.create(
812
904
  )
813
905
  ```
814
906
 
815
- **Deprecation notice:** The old field names `organizationId`, `organization_id`, `productId`, and `product_id` are still supported for backward compatibility but are deprecated. Use `organizationName` and `productName` for new implementations.
907
+ **Deprecation notice:** The legacy field aliases `organizationId`, `organization_id`, `productId`, and `product_id` are accepted by this SDK only as an input-layer convenience and emit a `DeprecationWarning`. The Revenium backend no longer accepts them — they are translated to `organizationName` / `productName` before the wire call. Migrate to `organization_name` / `organizationName` and `product_name` / `productName` now; the input-layer aliases will be removed in the next major release.
816
908
 
817
909
  **API Reference:** [Complete metadata field documentation](https://revenium.readme.io/reference/meter_ai_completion)
818
910
 
@@ -853,7 +945,7 @@ REVENIUM_TRACE_TYPE=customer-support
853
945
 
854
946
  ```python
855
947
  response = client.chat.completions.create(
856
- model="gpt-5.5",
948
+ model="gpt-4o-mini",
857
949
  messages=[{"role": "user", "content": "Hello!"}],
858
950
  usage_metadata={
859
951
  "environment": "production",
@@ -877,7 +969,7 @@ workflow_id = str(uuid.uuid4())
877
969
 
878
970
  # Step 1: Parent operation
879
971
  parent_response = client.chat.completions.create(
880
- model="gpt-5.5",
972
+ model="gpt-4o-mini",
881
973
  messages=[{"role": "user", "content": "Analyze this document"}],
882
974
  usage_metadata={
883
975
  "trace_id": "analysis-session-456",
@@ -888,7 +980,7 @@ parent_response = client.chat.completions.create(
888
980
 
889
981
  # Step 2: Child operation linked to parent
890
982
  child_response = client.chat.completions.create(
891
- model="gpt-5.5",
983
+ model="gpt-4o-mini",
892
984
  messages=[{"role": "user", "content": "Summarize findings"}],
893
985
  usage_metadata={
894
986
  "trace_id": "analysis-session-456",
@@ -919,7 +1011,7 @@ from revenium_middleware import revenium_metadata
919
1011
  def handle_customer_query(question: str) -> str:
920
1012
  # All API calls automatically include the decorator metadata
921
1013
  response = client.chat.completions.create(
922
- model="gpt-5.5",
1014
+ model="gpt-4o-mini",
923
1015
  messages=[{"role": "user", "content": question}]
924
1016
  )
925
1017
  return response.choices[0].message.content
@@ -958,13 +1050,13 @@ def outer_function():
958
1050
  def mixed_metadata():
959
1051
  # Uses decorator metadata
960
1052
  response1 = client.chat.completions.create(
961
- model="gpt-5.5",
1053
+ model="gpt-4o-mini",
962
1054
  messages=[{"role": "user", "content": "Hello"}]
963
1055
  )
964
1056
 
965
1057
  # API-level metadata overrides decorator's task_type
966
1058
  response2 = client.chat.completions.create(
967
- model="gpt-5.5",
1059
+ model="gpt-4o-mini",
968
1060
  messages=[{"role": "user", "content": "Hello"}],
969
1061
  usage_metadata={
970
1062
  "task_type": "special-override", # Overrides decorator
@@ -993,7 +1085,7 @@ from revenium_middleware import revenium_meter, revenium_metadata
993
1085
  def premium_feature(prompt: str) -> str:
994
1086
  # This WILL be metered (decorated with @revenium_meter)
995
1087
  response = client.chat.completions.create(
996
- model="gpt-5.5",
1088
+ model="gpt-4o-mini",
997
1089
  messages=[{"role": "user", "content": prompt}]
998
1090
  )
999
1091
  return response.choices[0].message.content
@@ -1001,7 +1093,7 @@ def premium_feature(prompt: str) -> str:
1001
1093
  def free_feature(prompt: str) -> str:
1002
1094
  # This will NOT be metered (no @revenium_meter decorator)
1003
1095
  response = client.chat.completions.create(
1004
- model="gpt-5.5",
1096
+ model="gpt-4o-mini",
1005
1097
  messages=[{"role": "user", "content": prompt}]
1006
1098
  )
1007
1099
  return response.choices[0].message.content
@@ -1087,7 +1179,7 @@ from openai import OpenAI
1087
1179
 
1088
1180
  client = OpenAI()
1089
1181
  response = client.chat.completions.create(
1090
- model="gpt-5.5",
1182
+ model="gpt-4o-mini",
1091
1183
  messages=[
1092
1184
  {"role": "system", "content": "You are a helpful assistant."},
1093
1185
  {"role": "user", "content": "What is the capital of France?"}
@@ -1109,62 +1201,6 @@ Prompt capture works with both streaming and non-streaming requests, and with mu
1109
1201
 
1110
1202
  ---
1111
1203
 
1112
- ## Terminal Summary Output
1113
-
1114
- Display a cost and usage summary in your terminal after each API request. Useful for development, debugging, and monitoring AI costs in real-time.
1115
-
1116
- ### Configuration
1117
-
1118
- | Environment Variable | Values | Description |
1119
- |---------------------|--------|-------------|
1120
- | `REVENIUM_PRINT_SUMMARY` | `false` (default), `true` or `human`, `json` | Controls output format |
1121
- | `REVENIUM_TEAM_ID` | Your team ID | Required to fetch and display cost information |
1122
-
1123
- ```bash
1124
- # Enable human-readable output
1125
- export REVENIUM_PRINT_SUMMARY=human
1126
-
1127
- # Required for cost display (find in Revenium web app)
1128
- export REVENIUM_TEAM_ID=your-team-id-here
1129
- ```
1130
-
1131
- ### Human-Readable Format
1132
-
1133
- ```
1134
- ============================================================
1135
- REVENIUM USAGE SUMMARY
1136
- ============================================================
1137
- Model: gpt-5.5
1138
- Provider: OPENAI
1139
- Duration: 1.23s
1140
-
1141
- Token Usage:
1142
- Input Tokens: 150
1143
- Output Tokens: 250
1144
- Total Tokens: 400
1145
-
1146
- Cost: $0.000045
1147
-
1148
- Trace ID: abc-123
1149
- ============================================================
1150
- ```
1151
-
1152
- ### JSON Format
1153
-
1154
- ```json
1155
- {"model":"gpt-5.5","provider":"OPENAI","durationSeconds":1.23,"inputTokenCount":150,"outputTokenCount":250,"totalTokenCount":400,"cost":0.000045,"costStatus":"available","traceId":"abc-123"}
1156
- ```
1157
-
1158
- ### Cost Status
1159
-
1160
- | Scenario | Display |
1161
- |----------|---------|
1162
- | Cost available | `$0.000045` |
1163
- | `REVENIUM_TEAM_ID` set, cost pending | `Pending (aggregating... check Revenium dashboard)` |
1164
- | `REVENIUM_TEAM_ID` not set | `Add REVENIUM_TEAM_ID to see pricing` |
1165
-
1166
- ---
1167
-
1168
1204
  ## Cost Controls / Enforcement
1169
1205
 
1170
1206
  Block outbound provider requests client-side when a Revenium cost control trips. When the circuit breaker is enabled, the middleware polls compiled enforcement rules from the Revenium API in a background daemon thread and raises `BudgetExceededError` **before** the upstream call, preventing spend beyond the configured limit.
@@ -1243,7 +1279,7 @@ client = openai.OpenAI()
1243
1279
 
1244
1280
  try:
1245
1281
  response = client.chat.completions.create(
1246
- model="gpt-5.5",
1282
+ model="gpt-4o-mini",
1247
1283
  messages=[{"role": "user", "content": "Summarize the meeting notes"}],
1248
1284
  )
1249
1285
  except BudgetExceededError as exc:
@@ -1282,9 +1318,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1282
1318
  | `REVENIUM_METERING_BASE_URL` | `https://api.revenium.ai` | Revenium API endpoint |
1283
1319
  | `REVENIUM_LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
1284
1320
  | `REVENIUM_CAPTURE_PROMPTS` | `false` | Enable prompt capture |
1285
- | `REVENIUM_PRINT_SUMMARY` | `false` | Terminal output: `false`, `true`/`human`, `json` |
1286
1321
  | `REVENIUM_SELECTIVE_METERING` | `false` | Only meter `@revenium_meter` decorated functions |
1287
- | `REVENIUM_TEAM_ID` | - | Team ID for cost display in terminal summary |
1322
+ | `REVENIUM_TEAM_ID` | - | Team ID for cost lookups |
1288
1323
  | `REVENIUM_ENVIRONMENT` | - | Deployment environment (auto-detects from `ENVIRONMENT`, `DEPLOYMENT_ENV`) |
1289
1324
  | `REVENIUM_REGION` | - | Cloud region (auto-detects from `AWS_REGION`, `AZURE_REGION`, `GCP_REGION`) |
1290
1325
  | `REVENIUM_CREDENTIAL_ALIAS` | - | Human-readable API key name |
@@ -106,7 +106,7 @@ import revenium_middleware_openai # Auto-initializes on import
106
106
 
107
107
  client = openai.OpenAI()
108
108
  response = client.chat.completions.create(
109
- model="gpt-5.5",
109
+ model="gpt-4o-mini",
110
110
  messages=[{"role": "user", "content": "Hello!"}]
111
111
  )
112
112
  print(response.choices[0].message.content)
@@ -139,6 +139,98 @@ See [`examples/agentic_outcomes/`](examples/agentic_outcomes/) for runnable demo
139
139
 
140
140
  ---
141
141
 
142
+ ## Idempotency
143
+
144
+ Every metering POST from the provider middleware automatically includes an `Idempotency-Key` header. If the Revenium API receives the same key with the same body within 24 hours, it returns the cached response instead of double-billing — making metering submissions safe to retry.
145
+
146
+ ### Default
147
+
148
+ A fresh UUID v4 is generated automatically for every metering call. No action required.
149
+
150
+ ### Override
151
+
152
+ Use the `idempotency_key` context manager to tie metering to a business-level identifier so the same logical operation never double-meters across retries:
153
+
154
+ ```python
155
+ from revenium_middleware import idempotency_key
156
+
157
+ with idempotency_key(f"order-{order_id}"):
158
+ response = openai.chat.completions.create(...)
159
+ ```
160
+
161
+ The context manager is backed by `contextvars`, so it scopes correctly across threads and asyncio tasks.
162
+
163
+ ### Backend behavior
164
+
165
+ | Scenario | Backend response |
166
+ | -- | -- |
167
+ | First call with key K and body B | Executes, caches for 24h |
168
+ | Retry with same K and same B | Returns cached response (no double-bill) |
169
+ | Same K with different B | `409 idempotency_key_mismatch` |
170
+ | Concurrent in-flight with same K | `409 idempotency_key_in_progress` + `Retry-After: 1` |
171
+ | Malformed key | `400 invalid_idempotency_key` |
172
+
173
+ See [docs.revenium.io/integrations/idempotency](https://docs.revenium.io/integrations/idempotency) for full backend semantics.
174
+
175
+ ### Key format
176
+
177
+ `Idempotency-Key` must be 1–255 printable ASCII characters. UUID v4 is the recommended format and what the SDK generates by default.
178
+
179
+ ---
180
+
181
+ ## Webhook Signature Verification
182
+
183
+ Revenium signs every outbound webhook with HMAC-SHA256 when a signing secret is configured. The SDK ships a verification helper so your handler can validate signatures without writing crypto.
184
+
185
+ Two headers arrive on every signed delivery:
186
+
187
+ | Header | Value |
188
+ | -- | -- |
189
+ | `X-Revenium-Signature-256` | `sha256=<hex>`. During a 24h rotation overlap: `sha256=A, sha256=B`. |
190
+ | `X-Revenium-Webhook-Timestamp` | Unix seconds at signing time. |
191
+
192
+ ### FastAPI example
193
+
194
+ ```python
195
+ import os
196
+
197
+ from fastapi import FastAPI, Header, HTTPException, Request
198
+
199
+ from revenium_middleware.webhooks import verify_signature
200
+
201
+ app = FastAPI()
202
+ SIGNING_SECRETS = [os.environ["REVENIUM_WEBHOOK_SECRET"]]
203
+
204
+
205
+ @app.post("/webhooks/revenium")
206
+ async def receive(
207
+ request: Request,
208
+ x_revenium_signature_256: str = Header(...),
209
+ x_revenium_webhook_timestamp: str = Header(...),
210
+ ):
211
+ body = await request.body()
212
+ if not verify_signature(
213
+ payload=body,
214
+ signature_header=x_revenium_signature_256,
215
+ timestamp_header=x_revenium_webhook_timestamp,
216
+ secrets=SIGNING_SECRETS,
217
+ ):
218
+ raise HTTPException(status_code=401, detail="Invalid signature")
219
+
220
+ # ... process the event
221
+ return {"ok": True}
222
+ ```
223
+
224
+ ### Secret rotation
225
+
226
+ When you rotate a signing secret in the Revenium dashboard with the default 24-hour overlap, both the old and new secrets are active simultaneously and every webhook is signed with both. Supply both values in `SIGNING_SECRETS` during the overlap window; remove the old one once it expires.
227
+
228
+ ### Webhooks without a signing secret
229
+
230
+ Webhook deliveries without a configured signing secret arrive without HMAC headers. If your endpoint receives both signed and unsigned traffic, branch on header presence: treat missing headers as legacy unsigned mode and missing-signature-on-signed-only endpoints as an authentication failure.
231
+
232
+ ---
233
+
142
234
  ## Provider Usage Guides
143
235
 
144
236
  ### OpenAI
@@ -156,7 +248,7 @@ client = openai.OpenAI()
156
248
 
157
249
  # Basic chat completion
158
250
  response = client.chat.completions.create(
159
- model="gpt-5.5",
251
+ model="gpt-4o-mini",
160
252
  messages=[{"role": "user", "content": "Hello!"}],
161
253
  usage_metadata={
162
254
  "organizationName": "AcmeCorp",
@@ -168,7 +260,7 @@ response = client.chat.completions.create(
168
260
 
169
261
  # Streaming
170
262
  stream = client.chat.completions.create(
171
- model="gpt-5.5",
263
+ model="gpt-4o-mini",
172
264
  messages=[{"role": "user", "content": "Tell me a story"}],
173
265
  stream=True
174
266
  )
@@ -459,7 +551,7 @@ litellm.api_base = os.getenv("LITELLM_PROXY_URL")
459
551
  litellm.api_key = os.getenv("LITELLM_API_KEY")
460
552
 
461
553
  response = litellm.completion(
462
- model="gpt-5.5",
554
+ model="gpt-4o-mini",
463
555
  messages=[{"role": "user", "content": "Hello!"}],
464
556
  usage_metadata={
465
557
  "organizationName": "AcmeCorp",
@@ -617,7 +709,7 @@ handler = ReveniumCallbackHandler(
617
709
  agent_name="support_agent"
618
710
  )
619
711
 
620
- llm = ChatOpenAI(model="gpt-5.5", callbacks=[handler])
712
+ llm = ChatOpenAI(model="gpt-4o-mini", callbacks=[handler])
621
713
  response = llm.invoke("Hello!")
622
714
  ```
623
715
 
@@ -656,7 +748,7 @@ result = agent.invoke(
656
748
  from revenium_middleware_langchain import AsyncReveniumCallbackHandler
657
749
 
658
750
  handler = AsyncReveniumCallbackHandler(trace_id="async-session")
659
- llm = ChatOpenAI(model="gpt-5.5", callbacks=[handler])
751
+ llm = ChatOpenAI(model="gpt-4o-mini", callbacks=[handler])
660
752
  response = await llm.ainvoke("Hello!")
661
753
  ```
662
754
 
@@ -702,7 +794,7 @@ Add business context to any API call by passing a `usage_metadata` dictionary. A
702
794
 
703
795
  ```python
704
796
  response = client.chat.completions.create(
705
- model="gpt-5.5",
797
+ model="gpt-4o-mini",
706
798
  messages=[{"role": "user", "content": "Hello!"}],
707
799
  usage_metadata={
708
800
  "trace_id": "conv-28a7e9d4",
@@ -724,7 +816,7 @@ response = client.chat.completions.create(
724
816
  )
725
817
  ```
726
818
 
727
- **Deprecation notice:** The old field names `organizationId`, `organization_id`, `productId`, and `product_id` are still supported for backward compatibility but are deprecated. Use `organizationName` and `productName` for new implementations.
819
+ **Deprecation notice:** The legacy field aliases `organizationId`, `organization_id`, `productId`, and `product_id` are accepted by this SDK only as an input-layer convenience and emit a `DeprecationWarning`. The Revenium backend no longer accepts them — they are translated to `organizationName` / `productName` before the wire call. Migrate to `organization_name` / `organizationName` and `product_name` / `productName` now; the input-layer aliases will be removed in the next major release.
728
820
 
729
821
  **API Reference:** [Complete metadata field documentation](https://revenium.readme.io/reference/meter_ai_completion)
730
822
 
@@ -765,7 +857,7 @@ REVENIUM_TRACE_TYPE=customer-support
765
857
 
766
858
  ```python
767
859
  response = client.chat.completions.create(
768
- model="gpt-5.5",
860
+ model="gpt-4o-mini",
769
861
  messages=[{"role": "user", "content": "Hello!"}],
770
862
  usage_metadata={
771
863
  "environment": "production",
@@ -789,7 +881,7 @@ workflow_id = str(uuid.uuid4())
789
881
 
790
882
  # Step 1: Parent operation
791
883
  parent_response = client.chat.completions.create(
792
- model="gpt-5.5",
884
+ model="gpt-4o-mini",
793
885
  messages=[{"role": "user", "content": "Analyze this document"}],
794
886
  usage_metadata={
795
887
  "trace_id": "analysis-session-456",
@@ -800,7 +892,7 @@ parent_response = client.chat.completions.create(
800
892
 
801
893
  # Step 2: Child operation linked to parent
802
894
  child_response = client.chat.completions.create(
803
- model="gpt-5.5",
895
+ model="gpt-4o-mini",
804
896
  messages=[{"role": "user", "content": "Summarize findings"}],
805
897
  usage_metadata={
806
898
  "trace_id": "analysis-session-456",
@@ -831,7 +923,7 @@ from revenium_middleware import revenium_metadata
831
923
  def handle_customer_query(question: str) -> str:
832
924
  # All API calls automatically include the decorator metadata
833
925
  response = client.chat.completions.create(
834
- model="gpt-5.5",
926
+ model="gpt-4o-mini",
835
927
  messages=[{"role": "user", "content": question}]
836
928
  )
837
929
  return response.choices[0].message.content
@@ -870,13 +962,13 @@ def outer_function():
870
962
  def mixed_metadata():
871
963
  # Uses decorator metadata
872
964
  response1 = client.chat.completions.create(
873
- model="gpt-5.5",
965
+ model="gpt-4o-mini",
874
966
  messages=[{"role": "user", "content": "Hello"}]
875
967
  )
876
968
 
877
969
  # API-level metadata overrides decorator's task_type
878
970
  response2 = client.chat.completions.create(
879
- model="gpt-5.5",
971
+ model="gpt-4o-mini",
880
972
  messages=[{"role": "user", "content": "Hello"}],
881
973
  usage_metadata={
882
974
  "task_type": "special-override", # Overrides decorator
@@ -905,7 +997,7 @@ from revenium_middleware import revenium_meter, revenium_metadata
905
997
  def premium_feature(prompt: str) -> str:
906
998
  # This WILL be metered (decorated with @revenium_meter)
907
999
  response = client.chat.completions.create(
908
- model="gpt-5.5",
1000
+ model="gpt-4o-mini",
909
1001
  messages=[{"role": "user", "content": prompt}]
910
1002
  )
911
1003
  return response.choices[0].message.content
@@ -913,7 +1005,7 @@ def premium_feature(prompt: str) -> str:
913
1005
  def free_feature(prompt: str) -> str:
914
1006
  # This will NOT be metered (no @revenium_meter decorator)
915
1007
  response = client.chat.completions.create(
916
- model="gpt-5.5",
1008
+ model="gpt-4o-mini",
917
1009
  messages=[{"role": "user", "content": prompt}]
918
1010
  )
919
1011
  return response.choices[0].message.content
@@ -999,7 +1091,7 @@ from openai import OpenAI
999
1091
 
1000
1092
  client = OpenAI()
1001
1093
  response = client.chat.completions.create(
1002
- model="gpt-5.5",
1094
+ model="gpt-4o-mini",
1003
1095
  messages=[
1004
1096
  {"role": "system", "content": "You are a helpful assistant."},
1005
1097
  {"role": "user", "content": "What is the capital of France?"}
@@ -1021,62 +1113,6 @@ Prompt capture works with both streaming and non-streaming requests, and with mu
1021
1113
 
1022
1114
  ---
1023
1115
 
1024
- ## Terminal Summary Output
1025
-
1026
- Display a cost and usage summary in your terminal after each API request. Useful for development, debugging, and monitoring AI costs in real-time.
1027
-
1028
- ### Configuration
1029
-
1030
- | Environment Variable | Values | Description |
1031
- |---------------------|--------|-------------|
1032
- | `REVENIUM_PRINT_SUMMARY` | `false` (default), `true` or `human`, `json` | Controls output format |
1033
- | `REVENIUM_TEAM_ID` | Your team ID | Required to fetch and display cost information |
1034
-
1035
- ```bash
1036
- # Enable human-readable output
1037
- export REVENIUM_PRINT_SUMMARY=human
1038
-
1039
- # Required for cost display (find in Revenium web app)
1040
- export REVENIUM_TEAM_ID=your-team-id-here
1041
- ```
1042
-
1043
- ### Human-Readable Format
1044
-
1045
- ```
1046
- ============================================================
1047
- REVENIUM USAGE SUMMARY
1048
- ============================================================
1049
- Model: gpt-5.5
1050
- Provider: OPENAI
1051
- Duration: 1.23s
1052
-
1053
- Token Usage:
1054
- Input Tokens: 150
1055
- Output Tokens: 250
1056
- Total Tokens: 400
1057
-
1058
- Cost: $0.000045
1059
-
1060
- Trace ID: abc-123
1061
- ============================================================
1062
- ```
1063
-
1064
- ### JSON Format
1065
-
1066
- ```json
1067
- {"model":"gpt-5.5","provider":"OPENAI","durationSeconds":1.23,"inputTokenCount":150,"outputTokenCount":250,"totalTokenCount":400,"cost":0.000045,"costStatus":"available","traceId":"abc-123"}
1068
- ```
1069
-
1070
- ### Cost Status
1071
-
1072
- | Scenario | Display |
1073
- |----------|---------|
1074
- | Cost available | `$0.000045` |
1075
- | `REVENIUM_TEAM_ID` set, cost pending | `Pending (aggregating... check Revenium dashboard)` |
1076
- | `REVENIUM_TEAM_ID` not set | `Add REVENIUM_TEAM_ID to see pricing` |
1077
-
1078
- ---
1079
-
1080
1116
  ## Cost Controls / Enforcement
1081
1117
 
1082
1118
  Block outbound provider requests client-side when a Revenium cost control trips. When the circuit breaker is enabled, the middleware polls compiled enforcement rules from the Revenium API in a background daemon thread and raises `BudgetExceededError` **before** the upstream call, preventing spend beyond the configured limit.
@@ -1155,7 +1191,7 @@ client = openai.OpenAI()
1155
1191
 
1156
1192
  try:
1157
1193
  response = client.chat.completions.create(
1158
- model="gpt-5.5",
1194
+ model="gpt-4o-mini",
1159
1195
  messages=[{"role": "user", "content": "Summarize the meeting notes"}],
1160
1196
  )
1161
1197
  except BudgetExceededError as exc:
@@ -1194,9 +1230,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
1194
1230
  | `REVENIUM_METERING_BASE_URL` | `https://api.revenium.ai` | Revenium API endpoint |
1195
1231
  | `REVENIUM_LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
1196
1232
  | `REVENIUM_CAPTURE_PROMPTS` | `false` | Enable prompt capture |
1197
- | `REVENIUM_PRINT_SUMMARY` | `false` | Terminal output: `false`, `true`/`human`, `json` |
1198
1233
  | `REVENIUM_SELECTIVE_METERING` | `false` | Only meter `@revenium_meter` decorated functions |
1199
- | `REVENIUM_TEAM_ID` | - | Team ID for cost display in terminal summary |
1234
+ | `REVENIUM_TEAM_ID` | - | Team ID for cost lookups |
1200
1235
  | `REVENIUM_ENVIRONMENT` | - | Deployment environment (auto-detects from `ENVIRONMENT`, `DEPLOYMENT_ENV`) |
1201
1236
  | `REVENIUM_REGION` | - | Cloud region (auto-detects from `AWS_REGION`, `AZURE_REGION`, `GCP_REGION`) |
1202
1237
  | `REVENIUM_CREDENTIAL_ALIAS` | - | Human-readable API key name |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "revenium-python-sdk"
7
- version = "0.1.5"
7
+ version = "0.1.7"
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"