revenium-python-sdk 0.1.0__py3-none-any.whl

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 (73) hide show
  1. revenium_middleware/__init__.py +184 -0
  2. revenium_middleware/_core/__init__.py +65 -0
  3. revenium_middleware/_core/config.py +165 -0
  4. revenium_middleware/_core/context.py +109 -0
  5. revenium_middleware/_core/decorators.py +202 -0
  6. revenium_middleware/_core/metering.py +207 -0
  7. revenium_middleware/_core/prompt_extraction.py +55 -0
  8. revenium_middleware/_core/subscriber.py +51 -0
  9. revenium_middleware/_core/trace_fields.py +265 -0
  10. revenium_middleware/anthropic/__init__.py +108 -0
  11. revenium_middleware/anthropic/bedrock_adapter.py +753 -0
  12. revenium_middleware/anthropic/config.py +29 -0
  13. revenium_middleware/anthropic/middleware.py +1070 -0
  14. revenium_middleware/anthropic/prompt_extractor.py +178 -0
  15. revenium_middleware/anthropic/provider.py +141 -0
  16. revenium_middleware/anthropic/summary_printer.py +286 -0
  17. revenium_middleware/anthropic/trace_fields.py +158 -0
  18. revenium_middleware/google/__init__.py +114 -0
  19. revenium_middleware/google/common/__init__.py +127 -0
  20. revenium_middleware/google/common/exceptions.py +137 -0
  21. revenium_middleware/google/common/protocols.py +192 -0
  22. revenium_middleware/google/common/summary_printer.py +271 -0
  23. revenium_middleware/google/common/trace_fields.py +205 -0
  24. revenium_middleware/google/common/types.py +208 -0
  25. revenium_middleware/google/common/utils.py +1111 -0
  26. revenium_middleware/google/config.py +64 -0
  27. revenium_middleware/google/google_ai/__init__.py +53 -0
  28. revenium_middleware/google/google_ai/middleware.py +667 -0
  29. revenium_middleware/google/google_ai/provider.py +135 -0
  30. revenium_middleware/google/prompt_extractor.py +396 -0
  31. revenium_middleware/google/vertex_ai/__init__.py +56 -0
  32. revenium_middleware/google/vertex_ai/middleware.py +1162 -0
  33. revenium_middleware/google/vertex_ai/provider.py +99 -0
  34. revenium_middleware/litellm/__init__.py +25 -0
  35. revenium_middleware/litellm/client/__init__.py +81 -0
  36. revenium_middleware/litellm/client/config.py +53 -0
  37. revenium_middleware/litellm/client/context.py +198 -0
  38. revenium_middleware/litellm/client/decorators.py +912 -0
  39. revenium_middleware/litellm/client/hooks.py +192 -0
  40. revenium_middleware/litellm/client/integrations/__init__.py +26 -0
  41. revenium_middleware/litellm/client/integrations/crewai.py +446 -0
  42. revenium_middleware/litellm/client/middleware.py +321 -0
  43. revenium_middleware/litellm/client/summary_printer.py +314 -0
  44. revenium_middleware/litellm/client/trace_fields.py +51 -0
  45. revenium_middleware/litellm/client/validation.py +207 -0
  46. revenium_middleware/litellm/proxy/__init__.py +25 -0
  47. revenium_middleware/litellm/proxy/middleware.py +217 -0
  48. revenium_middleware/ollama/__init__.py +28 -0
  49. revenium_middleware/ollama/middleware.py +569 -0
  50. revenium_middleware/ollama/trace_fields.py +63 -0
  51. revenium_middleware/openai/__init__.py +23 -0
  52. revenium_middleware/openai/azure_config.py +169 -0
  53. revenium_middleware/openai/azure_model_resolver.py +219 -0
  54. revenium_middleware/openai/config.py +45 -0
  55. revenium_middleware/openai/exceptions.py +115 -0
  56. revenium_middleware/openai/langchain/__init__.py +114 -0
  57. revenium_middleware/openai/langchain/_utils.py +129 -0
  58. revenium_middleware/openai/langchain/unified_handler.py +526 -0
  59. revenium_middleware/openai/middleware.py +1451 -0
  60. revenium_middleware/openai/prompt_extractor.py +173 -0
  61. revenium_middleware/openai/provider.py +170 -0
  62. revenium_middleware/openai/summary_printer.py +292 -0
  63. revenium_middleware/openai/trace_fields.py +98 -0
  64. revenium_middleware/perplexity/__init__.py +97 -0
  65. revenium_middleware/perplexity/middleware.py +379 -0
  66. revenium_middleware/perplexity/perplexity_sdk.py +256 -0
  67. revenium_middleware/perplexity/provider.py +84 -0
  68. revenium_middleware/perplexity/trace_fields.py +25 -0
  69. revenium_python_sdk-0.1.0.dist-info/METADATA +252 -0
  70. revenium_python_sdk-0.1.0.dist-info/RECORD +73 -0
  71. revenium_python_sdk-0.1.0.dist-info/WHEEL +5 -0
  72. revenium_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  73. revenium_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,912 @@
1
+ """
2
+ Decorator-based metadata injection for Revenium LiteLLM middleware.
3
+
4
+ This module provides convenient decorators for automatically injecting metadata
5
+ into LiteLLM completion calls. Decorators work with both sync and async functions.
6
+
7
+ Available decorators:
8
+ - track_agent: Set agent metadata
9
+ - track_task: Set task_type metadata
10
+ - track_trace: Set trace_id metadata
11
+ - track_organization: Set organization_name metadata
12
+ - track_subscription: Set subscription_id metadata
13
+ - track_product: Set product_name metadata
14
+ - track_subscriber: Set subscriber metadata (id, email, credential)
15
+ - track_quality: Set response_quality_score metadata
16
+
17
+ Example:
18
+ >>> from revenium_middleware.litellm.client import track_agent, track_task, track_trace
19
+ >>>
20
+ >>> @track_agent("Lead Analyst")
21
+ >>> @track_trace("workflow-123")
22
+ >>> def analyze_market():
23
+ ... response = litellm.completion(...)
24
+ ... return response
25
+ >>>
26
+ >>> @track_task("research")
27
+ >>> async def research_topic():
28
+ ... response = await litellm.acompletion(...)
29
+ ... return response
30
+ >>>
31
+ >>> # Dynamic attribute extraction
32
+ >>> @track_agent(name_from_arg="agent_name")
33
+ >>> def process_with_agent(agent_name, data):
34
+ ... response = litellm.completion(...)
35
+ ... return response
36
+ """
37
+
38
+ import functools
39
+ import inspect
40
+ from typing import Callable, Optional
41
+ from .context import metadata_context
42
+
43
+
44
+ def track_agent(
45
+ agent: Optional[str] = None,
46
+ *,
47
+ name_from_arg: Optional[str] = None,
48
+ name_from_attr: Optional[str] = None
49
+ ) -> Callable:
50
+ """
51
+ Decorator to automatically inject agent metadata into LiteLLM calls.
52
+
53
+ This decorator sets the 'agent' field in the metadata context for the
54
+ duration of the decorated function. It supports both static agent names
55
+ and dynamic extraction from function arguments or object attributes.
56
+
57
+ Works with both sync and async functions.
58
+
59
+ Args:
60
+ agent: Static agent name to use. Mutually exclusive with name_from_arg
61
+ and name_from_attr.
62
+ name_from_arg: Name of function argument to use as agent name.
63
+ The argument value will be converted to string.
64
+ name_from_attr: Name of object attribute to use as agent name.
65
+ Only works when decorating methods. Uses self.{attr}.
66
+
67
+ Returns:
68
+ Decorated function that sets agent metadata
69
+
70
+ Raises:
71
+ ValueError: If multiple or no agent sources are specified
72
+
73
+ Example:
74
+ >>> # Static agent name
75
+ >>> @track_agent("Lead Analyst")
76
+ >>> def analyze():
77
+ ... return litellm.completion(...)
78
+ >>>
79
+ >>> # Dynamic from argument
80
+ >>> @track_agent(name_from_arg="agent_name")
81
+ >>> def process(agent_name, data):
82
+ ... return litellm.completion(...)
83
+ >>>
84
+ >>> # Dynamic from object attribute
85
+ >>> class Agent:
86
+ ... def __init__(self, name):
87
+ ... self.name = name
88
+ ...
89
+ ... @track_agent(name_from_attr="name")
90
+ ... def execute(self):
91
+ ... return litellm.completion(...)
92
+ """
93
+ # Validate arguments
94
+ sources = sum([
95
+ agent is not None,
96
+ name_from_arg is not None,
97
+ name_from_attr is not None
98
+ ])
99
+
100
+ if sources == 0:
101
+ raise ValueError("Must specify agent, name_from_arg, or name_from_attr")
102
+ if sources > 1:
103
+ raise ValueError("Can only specify one of: agent, name_from_arg, name_from_attr")
104
+
105
+ def decorator(func: Callable) -> Callable:
106
+ # Check if function is async
107
+ is_async = inspect.iscoroutinefunction(func)
108
+
109
+ if is_async:
110
+ @functools.wraps(func)
111
+ async def async_wrapper(*args, **kwargs):
112
+ # Determine agent name
113
+ agent_name = _get_value(agent, name_from_arg, name_from_attr, args, kwargs, func, "agent")
114
+
115
+ # Set context and call function
116
+ with metadata_context.set(agent=agent_name):
117
+ return await func(*args, **kwargs)
118
+
119
+ return async_wrapper
120
+ else:
121
+ @functools.wraps(func)
122
+ def sync_wrapper(*args, **kwargs):
123
+ # Determine agent name
124
+ agent_name = _get_value(agent, name_from_arg, name_from_attr, args, kwargs, func, "agent")
125
+
126
+ # Set context and call function
127
+ with metadata_context.set(agent=agent_name):
128
+ return func(*args, **kwargs)
129
+
130
+ return sync_wrapper
131
+
132
+ return decorator
133
+
134
+
135
+ def track_task(
136
+ task_type: Optional[str] = None,
137
+ *,
138
+ type_from_arg: Optional[str] = None,
139
+ type_from_attr: Optional[str] = None
140
+ ) -> Callable:
141
+ """
142
+ Decorator to automatically inject task_type metadata into LiteLLM calls.
143
+
144
+ This decorator sets the 'task_type' field in the metadata context for the
145
+ duration of the decorated function. It supports both static task types
146
+ and dynamic extraction from function arguments or object attributes.
147
+
148
+ Works with both sync and async functions.
149
+
150
+ Args:
151
+ task_type: Static task type to use. Mutually exclusive with type_from_arg
152
+ and type_from_attr.
153
+ type_from_arg: Name of function argument to use as task type.
154
+ The argument value will be converted to string.
155
+ type_from_attr: Name of object attribute to use as task type.
156
+ Only works when decorating methods. Uses self.{attr}.
157
+
158
+ Returns:
159
+ Decorated function that sets task_type metadata
160
+
161
+ Raises:
162
+ ValueError: If multiple or no task type sources are specified
163
+
164
+ Example:
165
+ >>> # Static task type
166
+ >>> @track_task("research")
167
+ >>> def research():
168
+ ... return litellm.completion(...)
169
+ >>>
170
+ >>> # Dynamic from argument
171
+ >>> @track_task(type_from_arg="operation")
172
+ >>> def process(operation, data):
173
+ ... return litellm.completion(...)
174
+ >>>
175
+ >>> # Dynamic from object attribute
176
+ >>> class Task:
177
+ ... def __init__(self, task_type):
178
+ ... self.task_type = task_type
179
+ ...
180
+ ... @track_task(type_from_attr="task_type")
181
+ ... def execute(self):
182
+ ... return litellm.completion(...)
183
+ """
184
+ # Validate arguments
185
+ sources = sum([
186
+ task_type is not None,
187
+ type_from_arg is not None,
188
+ type_from_attr is not None
189
+ ])
190
+
191
+ if sources == 0:
192
+ raise ValueError("Must specify task_type, type_from_arg, or type_from_attr")
193
+ if sources > 1:
194
+ raise ValueError("Can only specify one of: task_type, type_from_arg, type_from_attr")
195
+
196
+ def decorator(func: Callable) -> Callable:
197
+ # Check if function is async
198
+ is_async = inspect.iscoroutinefunction(func)
199
+
200
+ if is_async:
201
+ @functools.wraps(func)
202
+ async def async_wrapper(*args, **kwargs):
203
+ # Determine task type
204
+ task_type_value = _get_value(task_type, type_from_arg, type_from_attr, args, kwargs, func, "task_type")
205
+
206
+ # Set context and call function
207
+ with metadata_context.set(task_type=task_type_value):
208
+ return await func(*args, **kwargs)
209
+
210
+ return async_wrapper
211
+ else:
212
+ @functools.wraps(func)
213
+ def sync_wrapper(*args, **kwargs):
214
+ # Determine task type
215
+ task_type_value = _get_value(task_type, type_from_arg, type_from_attr, args, kwargs, func, "task_type")
216
+
217
+ # Set context and call function
218
+ with metadata_context.set(task_type=task_type_value):
219
+ return func(*args, **kwargs)
220
+
221
+ return sync_wrapper
222
+
223
+ return decorator
224
+
225
+
226
+ def _get_value(
227
+ static_value: Optional[str],
228
+ arg_name: Optional[str],
229
+ attr_name: Optional[str],
230
+ args: tuple,
231
+ kwargs: dict,
232
+ func: Callable,
233
+ field_name: str
234
+ ) -> str:
235
+ """
236
+ Generic helper to extract a value from static value, argument, or attribute.
237
+
238
+ Args:
239
+ static_value: Static value
240
+ arg_name: Argument name to extract from
241
+ attr_name: Attribute name to extract from
242
+ args: Positional arguments
243
+ kwargs: Keyword arguments
244
+ func: The decorated function
245
+ field_name: Name of the field (for error messages)
246
+
247
+ Returns:
248
+ Value as string
249
+
250
+ Raises:
251
+ ValueError: If argument/attribute not found or invalid
252
+ """
253
+ if static_value is not None:
254
+ return static_value
255
+
256
+ if arg_name is not None:
257
+ # Try to get from kwargs first
258
+ if arg_name in kwargs:
259
+ return str(kwargs[arg_name])
260
+
261
+ # Try to get from args using function signature
262
+ sig = inspect.signature(func)
263
+ param_names = list(sig.parameters.keys())
264
+
265
+ if arg_name in param_names:
266
+ arg_index = param_names.index(arg_name)
267
+ if arg_index < len(args):
268
+ return str(args[arg_index])
269
+
270
+ raise ValueError(f"Argument '{arg_name}' not found in function call for {field_name}")
271
+
272
+ if attr_name is not None:
273
+ # Get from self (first argument for methods)
274
+ if len(args) == 0:
275
+ raise ValueError(f"Cannot use attr on function without self argument for {field_name}")
276
+
277
+ self_obj = args[0]
278
+ if not hasattr(self_obj, attr_name):
279
+ raise ValueError(f"Object does not have attribute '{attr_name}' for {field_name}")
280
+
281
+ return str(getattr(self_obj, attr_name))
282
+
283
+ raise ValueError(f"No {field_name} source specified")
284
+
285
+
286
+ def track_trace(
287
+ trace_id: Optional[str] = None,
288
+ *,
289
+ id_from_arg: Optional[str] = None,
290
+ id_from_attr: Optional[str] = None
291
+ ) -> Callable:
292
+ """
293
+ Decorator to automatically inject trace_id metadata into LiteLLM calls.
294
+
295
+ This decorator sets the 'trace_id' field in the metadata context for the
296
+ duration of the decorated function. It supports both static trace IDs
297
+ and dynamic extraction from function arguments or object attributes.
298
+
299
+ Works with both sync and async functions.
300
+
301
+ Args:
302
+ trace_id: Static trace ID to use. Mutually exclusive with id_from_arg
303
+ and id_from_attr.
304
+ id_from_arg: Name of function argument to use as trace ID.
305
+ The argument value will be converted to string.
306
+ id_from_attr: Name of object attribute to use as trace ID.
307
+ Only works when decorating methods. Uses self.{attr}.
308
+
309
+ Returns:
310
+ Decorated function that sets trace_id metadata
311
+
312
+ Raises:
313
+ ValueError: If multiple or no trace ID sources are specified
314
+
315
+ Example:
316
+ >>> # Static trace ID
317
+ >>> @track_trace("workflow-123")
318
+ >>> def process():
319
+ ... return litellm.completion(...)
320
+ >>>
321
+ >>> # Dynamic from argument
322
+ >>> @track_trace(id_from_arg="workflow_id")
323
+ >>> def process(workflow_id, data):
324
+ ... return litellm.completion(...)
325
+ """
326
+ # Validate arguments
327
+ sources = sum([
328
+ trace_id is not None,
329
+ id_from_arg is not None,
330
+ id_from_attr is not None
331
+ ])
332
+
333
+ if sources == 0:
334
+ raise ValueError("Must specify trace_id, id_from_arg, or id_from_attr")
335
+ if sources > 1:
336
+ raise ValueError("Can only specify one of: trace_id, id_from_arg, id_from_attr")
337
+
338
+ def decorator(func: Callable) -> Callable:
339
+ # Check if function is async
340
+ is_async = inspect.iscoroutinefunction(func)
341
+
342
+ if is_async:
343
+ @functools.wraps(func)
344
+ async def async_wrapper(*args, **kwargs):
345
+ # Determine trace ID
346
+ trace_id_value = _get_value(trace_id, id_from_arg, id_from_attr, args, kwargs, func, "trace_id")
347
+
348
+ # Set context and call function
349
+ with metadata_context.set(trace_id=trace_id_value):
350
+ return await func(*args, **kwargs)
351
+
352
+ return async_wrapper
353
+ else:
354
+ @functools.wraps(func)
355
+ def sync_wrapper(*args, **kwargs):
356
+ # Determine trace ID
357
+ trace_id_value = _get_value(trace_id, id_from_arg, id_from_attr, args, kwargs, func, "trace_id")
358
+
359
+ # Set context and call function
360
+ with metadata_context.set(trace_id=trace_id_value):
361
+ return func(*args, **kwargs)
362
+
363
+ return sync_wrapper
364
+
365
+ return decorator
366
+
367
+
368
+ def track_organization(
369
+ organization_name: Optional[str] = None,
370
+ *,
371
+ name_from_arg: Optional[str] = None,
372
+ name_from_attr: Optional[str] = None,
373
+ # Deprecated parameter names for backward compatibility
374
+ organization_id: Optional[str] = None,
375
+ id_from_arg: Optional[str] = None,
376
+ id_from_attr: Optional[str] = None
377
+ ) -> Callable:
378
+ """
379
+ Decorator to automatically inject organization_name metadata into LiteLLM calls.
380
+
381
+ This decorator sets the 'organization_name' field in the metadata context for the
382
+ duration of the decorated function. It supports both static organization names
383
+ and dynamic extraction from function arguments or object attributes.
384
+
385
+ Works with both sync and async functions.
386
+
387
+ Args:
388
+ organization_name: Static organization name to use. Mutually exclusive with
389
+ name_from_arg and name_from_attr.
390
+ name_from_arg: Name of function argument to use as organization name.
391
+ The argument value will be converted to string.
392
+ name_from_attr: Name of object attribute to use as organization name.
393
+ Only works when decorating methods. Uses self.{attr}.
394
+ organization_id: Deprecated. Use organization_name instead.
395
+ id_from_arg: Deprecated. Use name_from_arg instead.
396
+ id_from_attr: Deprecated. Use name_from_attr instead.
397
+
398
+ Returns:
399
+ Decorated function that sets organization_name metadata
400
+
401
+ Raises:
402
+ ValueError: If multiple or no organization name sources are specified
403
+
404
+ Example:
405
+ >>> # Static organization name
406
+ >>> @track_organization("AcmeCorp")
407
+ >>> def process():
408
+ ... return litellm.completion(...)
409
+ >>>
410
+ >>> # Dynamic from argument
411
+ >>> @track_organization(name_from_arg="org_name")
412
+ >>> def process(org_name, data):
413
+ ... return litellm.completion(...)
414
+ """
415
+ # Support deprecated parameter names (new names take precedence)
416
+ effective_name = organization_name if organization_name is not None else organization_id
417
+ effective_from_arg = name_from_arg if name_from_arg is not None else id_from_arg
418
+ effective_from_attr = name_from_attr if name_from_attr is not None else id_from_attr
419
+
420
+ # Validate arguments
421
+ sources = sum([
422
+ effective_name is not None,
423
+ effective_from_arg is not None,
424
+ effective_from_attr is not None
425
+ ])
426
+
427
+ if sources == 0:
428
+ raise ValueError("Must specify organization_name, name_from_arg, or name_from_attr")
429
+ if sources > 1:
430
+ raise ValueError("Can only specify one of: organization_name, name_from_arg, name_from_attr")
431
+
432
+ def decorator(func: Callable) -> Callable:
433
+ # Check if function is async
434
+ is_async = inspect.iscoroutinefunction(func)
435
+
436
+ if is_async:
437
+ @functools.wraps(func)
438
+ async def async_wrapper(*args, **kwargs):
439
+ # Determine organization name
440
+ org_name_value = _get_value(effective_name, effective_from_arg, effective_from_attr, args, kwargs, func, "organization_name")
441
+
442
+ # Set context using organization_name (not organization_id)
443
+ with metadata_context.set(organization_name=org_name_value):
444
+ return await func(*args, **kwargs)
445
+
446
+ return async_wrapper
447
+ else:
448
+ @functools.wraps(func)
449
+ def sync_wrapper(*args, **kwargs):
450
+ # Determine organization name
451
+ org_name_value = _get_value(effective_name, effective_from_arg, effective_from_attr, args, kwargs, func, "organization_name")
452
+
453
+ # Set context using organization_name (not organization_id)
454
+ with metadata_context.set(organization_name=org_name_value):
455
+ return func(*args, **kwargs)
456
+
457
+ return sync_wrapper
458
+
459
+ return decorator
460
+
461
+
462
+ def track_subscription(
463
+ subscription_id: Optional[str] = None,
464
+ *,
465
+ id_from_arg: Optional[str] = None,
466
+ id_from_attr: Optional[str] = None
467
+ ) -> Callable:
468
+ """
469
+ Decorator to automatically inject subscription_id metadata into LiteLLM calls.
470
+
471
+ This decorator sets the 'subscription_id' field in the metadata context for the
472
+ duration of the decorated function. It supports both static subscription IDs
473
+ and dynamic extraction from function arguments or object attributes.
474
+
475
+ Works with both sync and async functions.
476
+
477
+ Args:
478
+ subscription_id: Static subscription ID to use. Mutually exclusive with
479
+ id_from_arg and id_from_attr.
480
+ id_from_arg: Name of function argument to use as subscription ID.
481
+ The argument value will be converted to string.
482
+ id_from_attr: Name of object attribute to use as subscription ID.
483
+ Only works when decorating methods. Uses self.{attr}.
484
+
485
+ Returns:
486
+ Decorated function that sets subscription_id metadata
487
+
488
+ Raises:
489
+ ValueError: If multiple or no subscription ID sources are specified
490
+
491
+ Example:
492
+ >>> # Static subscription ID
493
+ >>> @track_subscription("sub-123")
494
+ >>> def process():
495
+ ... return litellm.completion(...)
496
+ >>>
497
+ >>> # Dynamic from argument
498
+ >>> @track_subscription(id_from_arg="sub_id")
499
+ >>> def process(sub_id, data):
500
+ ... return litellm.completion(...)
501
+ """
502
+ # Validate arguments
503
+ sources = sum([
504
+ subscription_id is not None,
505
+ id_from_arg is not None,
506
+ id_from_attr is not None
507
+ ])
508
+
509
+ if sources == 0:
510
+ raise ValueError("Must specify subscription_id, id_from_arg, or id_from_attr")
511
+ if sources > 1:
512
+ raise ValueError("Can only specify one of: subscription_id, id_from_arg, id_from_attr")
513
+
514
+ def decorator(func: Callable) -> Callable:
515
+ # Check if function is async
516
+ is_async = inspect.iscoroutinefunction(func)
517
+
518
+ if is_async:
519
+ @functools.wraps(func)
520
+ async def async_wrapper(*args, **kwargs):
521
+ # Determine subscription ID
522
+ sub_id_value = _get_value(subscription_id, id_from_arg, id_from_attr, args, kwargs, func, "subscription_id")
523
+
524
+ # Set context and call function
525
+ with metadata_context.set(subscription_id=sub_id_value):
526
+ return await func(*args, **kwargs)
527
+
528
+ return async_wrapper
529
+ else:
530
+ @functools.wraps(func)
531
+ def sync_wrapper(*args, **kwargs):
532
+ # Determine subscription ID
533
+ sub_id_value = _get_value(subscription_id, id_from_arg, id_from_attr, args, kwargs, func, "subscription_id")
534
+
535
+ # Set context and call function
536
+ with metadata_context.set(subscription_id=sub_id_value):
537
+ return func(*args, **kwargs)
538
+
539
+ return sync_wrapper
540
+
541
+ return decorator
542
+
543
+
544
+ def track_product(
545
+ product_name: Optional[str] = None,
546
+ *,
547
+ name_from_arg: Optional[str] = None,
548
+ name_from_attr: Optional[str] = None,
549
+ # Deprecated parameter names for backward compatibility
550
+ product_id: Optional[str] = None,
551
+ id_from_arg: Optional[str] = None,
552
+ id_from_attr: Optional[str] = None
553
+ ) -> Callable:
554
+ """
555
+ Decorator to automatically inject product_name metadata into LiteLLM calls.
556
+
557
+ This decorator sets the 'product_name' field in the metadata context for the
558
+ duration of the decorated function. It supports both static product names
559
+ and dynamic extraction from function arguments or object attributes.
560
+
561
+ Works with both sync and async functions.
562
+
563
+ Args:
564
+ product_name: Static product name to use. Mutually exclusive with
565
+ name_from_arg and name_from_attr.
566
+ name_from_arg: Name of function argument to use as product name.
567
+ The argument value will be converted to string.
568
+ name_from_attr: Name of object attribute to use as product name.
569
+ Only works when decorating methods. Uses self.{attr}.
570
+ product_id: Deprecated. Use product_name instead.
571
+ id_from_arg: Deprecated. Use name_from_arg instead.
572
+ id_from_attr: Deprecated. Use name_from_attr instead.
573
+
574
+ Returns:
575
+ Decorated function that sets product_name metadata
576
+
577
+ Raises:
578
+ ValueError: If multiple or no product name sources are specified
579
+
580
+ Example:
581
+ >>> # Static product name
582
+ >>> @track_product("ai-assistant")
583
+ >>> def process():
584
+ ... return litellm.completion(...)
585
+ >>>
586
+ >>> # Dynamic from argument
587
+ >>> @track_product(name_from_arg="prod_name")
588
+ >>> def process(prod_name, data):
589
+ ... return litellm.completion(...)
590
+ """
591
+ # Support deprecated parameter names (new names take precedence)
592
+ effective_name = product_name if product_name is not None else product_id
593
+ effective_from_arg = name_from_arg if name_from_arg is not None else id_from_arg
594
+ effective_from_attr = name_from_attr if name_from_attr is not None else id_from_attr
595
+
596
+ # Validate arguments
597
+ sources = sum([
598
+ effective_name is not None,
599
+ effective_from_arg is not None,
600
+ effective_from_attr is not None
601
+ ])
602
+
603
+ if sources == 0:
604
+ raise ValueError("Must specify product_name, name_from_arg, or name_from_attr")
605
+ if sources > 1:
606
+ raise ValueError("Can only specify one of: product_name, name_from_arg, name_from_attr")
607
+
608
+ def decorator(func: Callable) -> Callable:
609
+ # Check if function is async
610
+ is_async = inspect.iscoroutinefunction(func)
611
+
612
+ if is_async:
613
+ @functools.wraps(func)
614
+ async def async_wrapper(*args, **kwargs):
615
+ # Determine product name
616
+ prod_name_value = _get_value(effective_name, effective_from_arg, effective_from_attr, args, kwargs, func, "product_name")
617
+
618
+ # Set context using product_name (not product_id)
619
+ with metadata_context.set(product_name=prod_name_value):
620
+ return await func(*args, **kwargs)
621
+
622
+ return async_wrapper
623
+ else:
624
+ @functools.wraps(func)
625
+ def sync_wrapper(*args, **kwargs):
626
+ # Determine product name
627
+ prod_name_value = _get_value(effective_name, effective_from_arg, effective_from_attr, args, kwargs, func, "product_name")
628
+
629
+ # Set context using product_name (not product_id)
630
+ with metadata_context.set(product_name=prod_name_value):
631
+ return func(*args, **kwargs)
632
+
633
+ return sync_wrapper
634
+
635
+ return decorator
636
+
637
+
638
+ def _build_subscriber_dict(
639
+ subscriber_id: Optional[str],
640
+ subscriber_email: Optional[str],
641
+ credential_name: Optional[str],
642
+ id_from_arg: Optional[str],
643
+ email_from_arg: Optional[str],
644
+ credential_from_arg: Optional[str],
645
+ args: tuple,
646
+ kwargs: dict,
647
+ func: Callable
648
+ ) -> dict:
649
+ """
650
+ Helper function to build subscriber dictionary from various sources.
651
+
652
+ Args:
653
+ subscriber_id: Static subscriber ID
654
+ subscriber_email: Static subscriber email
655
+ credential_name: Static credential name
656
+ id_from_arg: Argument name for subscriber ID
657
+ email_from_arg: Argument name for subscriber email
658
+ credential_from_arg: Argument name for credential
659
+ args: Positional arguments
660
+ kwargs: Keyword arguments
661
+ func: The decorated function
662
+
663
+ Returns:
664
+ Dictionary with subscriber information
665
+ """
666
+ subscriber = {}
667
+
668
+ # Get subscriber ID
669
+ if subscriber_id is not None:
670
+ subscriber['id'] = subscriber_id
671
+ elif id_from_arg is not None:
672
+ try:
673
+ subscriber['id'] = _get_value(None, id_from_arg, None, args, kwargs, func, "subscriber_id")
674
+ except ValueError:
675
+ pass # Optional field, skip if not found
676
+
677
+ # Get subscriber email
678
+ if subscriber_email is not None:
679
+ subscriber['email'] = subscriber_email
680
+ elif email_from_arg is not None:
681
+ try:
682
+ subscriber['email'] = _get_value(None, email_from_arg, None, args, kwargs, func, "subscriber_email")
683
+ except ValueError:
684
+ pass # Optional field, skip if not found
685
+
686
+ # Get credential name
687
+ if credential_name is not None:
688
+ subscriber['credential'] = {'name': credential_name}
689
+ elif credential_from_arg is not None:
690
+ try:
691
+ cred_name = _get_value(None, credential_from_arg, None, args, kwargs, func, "credential_name")
692
+ subscriber['credential'] = {'name': cred_name}
693
+ except ValueError:
694
+ pass # Optional field, skip if not found
695
+
696
+ return subscriber
697
+
698
+
699
+ def track_subscriber(
700
+ subscriber_id: Optional[str] = None,
701
+ subscriber_email: Optional[str] = None,
702
+ credential_name: Optional[str] = None,
703
+ *,
704
+ id_from_arg: Optional[str] = None,
705
+ email_from_arg: Optional[str] = None,
706
+ credential_from_arg: Optional[str] = None
707
+ ) -> Callable:
708
+ """
709
+ Decorator to automatically inject subscriber metadata into LiteLLM calls.
710
+
711
+ This decorator sets the 'subscriber' field (with id, email, and credential)
712
+ in the metadata context for the duration of the decorated function.
713
+
714
+ Works with both sync and async functions.
715
+
716
+ Args:
717
+ subscriber_id: Static subscriber ID to use.
718
+ subscriber_email: Static subscriber email to use.
719
+ credential_name: Static credential name to use.
720
+ id_from_arg: Name of function argument to use as subscriber ID.
721
+ email_from_arg: Name of function argument to use as subscriber email.
722
+ credential_from_arg: Name of function argument to use as credential name.
723
+
724
+ Returns:
725
+ Decorated function that sets subscriber metadata
726
+
727
+ Example:
728
+ >>> # Static subscriber info
729
+ >>> @track_subscriber(subscriber_id="user-123", subscriber_email="user@example.com")
730
+ >>> def process():
731
+ ... return litellm.completion(...)
732
+ >>>
733
+ >>> # Dynamic from arguments
734
+ >>> @track_subscriber(id_from_arg="user_id", email_from_arg="user_email")
735
+ >>> def process(user_id, user_email, data):
736
+ ... return litellm.completion(...)
737
+ """
738
+ def decorator(func: Callable) -> Callable:
739
+ # Check if function is async
740
+ is_async = inspect.iscoroutinefunction(func)
741
+
742
+ if is_async:
743
+ @functools.wraps(func)
744
+ async def async_wrapper(*args, **kwargs):
745
+ # Build subscriber dict
746
+ subscriber = _build_subscriber_dict(
747
+ subscriber_id, subscriber_email, credential_name,
748
+ id_from_arg, email_from_arg, credential_from_arg,
749
+ args, kwargs, func
750
+ )
751
+
752
+ # Set context and call function
753
+ if subscriber:
754
+ with metadata_context.set(subscriber=subscriber):
755
+ return await func(*args, **kwargs)
756
+ else:
757
+ return await func(*args, **kwargs)
758
+
759
+ return async_wrapper
760
+ else:
761
+ @functools.wraps(func)
762
+ def sync_wrapper(*args, **kwargs):
763
+ # Build subscriber dict
764
+ subscriber = _build_subscriber_dict(
765
+ subscriber_id, subscriber_email, credential_name,
766
+ id_from_arg, email_from_arg, credential_from_arg,
767
+ args, kwargs, func
768
+ )
769
+
770
+ # Set context and call function
771
+ if subscriber:
772
+ with metadata_context.set(subscriber=subscriber):
773
+ return func(*args, **kwargs)
774
+ else:
775
+ return func(*args, **kwargs)
776
+
777
+ return sync_wrapper
778
+
779
+ return decorator
780
+
781
+
782
+ def track_quality(
783
+ quality_score: Optional[float] = None,
784
+ *,
785
+ score_from_arg: Optional[str] = None,
786
+ score_from_attr: Optional[str] = None
787
+ ) -> Callable:
788
+ """
789
+ Decorator to automatically inject response_quality_score metadata into LiteLLM calls.
790
+
791
+ This decorator sets the 'response_quality_score' field in the metadata context
792
+ for the duration of the decorated function. It supports both static quality scores
793
+ and dynamic extraction from function arguments or object attributes.
794
+
795
+ Works with both sync and async functions.
796
+
797
+ Args:
798
+ quality_score: Static quality score to use (0.0-1.0). Mutually exclusive with
799
+ score_from_arg and score_from_attr.
800
+ score_from_arg: Name of function argument to use as quality score.
801
+ The argument value will be converted to float.
802
+ score_from_attr: Name of object attribute to use as quality score.
803
+ Only works when decorating methods. Uses self.{attr}.
804
+
805
+ Returns:
806
+ Decorated function that sets response_quality_score metadata
807
+
808
+ Raises:
809
+ ValueError: If multiple or no quality score sources are specified
810
+
811
+ Example:
812
+ >>> # Static quality score
813
+ >>> @track_quality(0.95)
814
+ >>> def process():
815
+ ... return litellm.completion(...)
816
+ >>>
817
+ >>> # Dynamic from argument
818
+ >>> @track_quality(score_from_arg="min_quality")
819
+ >>> def process(min_quality, data):
820
+ ... return litellm.completion(...)
821
+ """
822
+ # Validate arguments
823
+ sources = sum([
824
+ quality_score is not None,
825
+ score_from_arg is not None,
826
+ score_from_attr is not None
827
+ ])
828
+
829
+ if sources == 0:
830
+ raise ValueError("Must specify quality_score, score_from_arg, or score_from_attr")
831
+ if sources > 1:
832
+ raise ValueError("Can only specify one of: quality_score, score_from_arg, score_from_attr")
833
+
834
+ def decorator(func: Callable) -> Callable:
835
+ # Check if function is async
836
+ is_async = inspect.iscoroutinefunction(func)
837
+
838
+ if is_async:
839
+ @functools.wraps(func)
840
+ async def async_wrapper(*args, **kwargs):
841
+ # Determine quality score
842
+ score_value = _get_quality_score(quality_score, score_from_arg, score_from_attr, args, kwargs, func)
843
+
844
+ # Set context and call function
845
+ with metadata_context.set(response_quality_score=score_value):
846
+ return await func(*args, **kwargs)
847
+
848
+ return async_wrapper
849
+ else:
850
+ @functools.wraps(func)
851
+ def sync_wrapper(*args, **kwargs):
852
+ # Determine quality score
853
+ score_value = _get_quality_score(quality_score, score_from_arg, score_from_attr, args, kwargs, func)
854
+
855
+ # Set context and call function
856
+ with metadata_context.set(response_quality_score=score_value):
857
+ return func(*args, **kwargs)
858
+
859
+ return sync_wrapper
860
+
861
+ return decorator
862
+
863
+
864
+ def _get_quality_score(
865
+ static_score: Optional[float],
866
+ arg_name: Optional[str],
867
+ attr_name: Optional[str],
868
+ args: tuple,
869
+ kwargs: dict,
870
+ func: Callable
871
+ ) -> float:
872
+ """
873
+ Extract quality score from static value, argument, or attribute.
874
+
875
+ Args:
876
+ static_score: Static quality score
877
+ arg_name: Argument name to extract from
878
+ attr_name: Attribute name to extract from
879
+ args: Positional arguments
880
+ kwargs: Keyword arguments
881
+ func: The decorated function
882
+
883
+ Returns:
884
+ Quality score as float
885
+
886
+ Raises:
887
+ ValueError: If argument/attribute not found or invalid
888
+ """
889
+ # Use the generic _get_value function and convert to float
890
+ value_str = _get_value(
891
+ str(static_score) if static_score is not None else None,
892
+ arg_name,
893
+ attr_name,
894
+ args,
895
+ kwargs,
896
+ func,
897
+ "quality_score"
898
+ )
899
+ return float(value_str)
900
+
901
+
902
+ __all__ = [
903
+ 'track_agent',
904
+ 'track_task',
905
+ 'track_trace',
906
+ 'track_organization',
907
+ 'track_subscription',
908
+ 'track_product',
909
+ 'track_subscriber',
910
+ 'track_quality',
911
+ ]
912
+