paid-python 0.0.5a39__py3-none-any.whl → 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.
@@ -1,14 +1,10 @@
1
1
  from typing import Any, Optional
2
2
 
3
- from opentelemetry import trace
4
- from opentelemetry.trace import Status, StatusCode
3
+ from opentelemetry.trace import Span, Status, StatusCode
5
4
 
5
+ from paid.logger import logger
6
6
  from paid.tracing.tracing import (
7
7
  get_paid_tracer,
8
- logger,
9
- paid_external_agent_id_var,
10
- paid_external_customer_id_var,
11
- paid_token_var,
12
8
  )
13
9
 
14
10
  try:
@@ -22,7 +18,7 @@ except ImportError:
22
18
 
23
19
  # Global dictionary to store spans keyed by context object ID
24
20
  # This avoids polluting user's context.context and works across async boundaries
25
- _paid_span_store: dict[int, trace.Span] = {}
21
+ _paid_span_store: dict[int, Span] = {}
26
22
 
27
23
 
28
24
  class PaidOpenAIAgentsHook(RunHooks[Any]):
@@ -32,14 +28,12 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
32
28
  Can optionally wrap user-provided hooks to combine Paid tracking with custom behavior.
33
29
  """
34
30
 
35
- def __init__(self, user_hooks: Optional[RunHooks[Any]] = None, optional_tracing: bool = False):
31
+ def __init__(self, user_hooks: Optional[RunHooks[Any]] = None):
36
32
  """
37
33
  Initialize PaidAgentsHook.
38
34
 
39
35
  Args:
40
36
  user_hooks: Optional user-provided RunHooks to combine with Paid tracking
41
- optional_tracing: If True, gracefully skip tracing when context is missing.
42
- If False, raise errors when tracing context is not available.
43
37
 
44
38
  Usage:
45
39
  @paid_tracing("<ext_customer_id>", "<ext_agent_id>")
@@ -55,67 +49,26 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
55
49
 
56
50
  my_hook = MyHook()
57
51
  hook = PaidAgentsHook(user_hooks=my_hook)
58
-
59
- # Optional tracing (won't raise errors if context missing)
60
- hook = PaidAgentsHook(optional_tracing=True)
61
52
  """
62
53
  super().__init__()
63
- self.tracer = get_paid_tracer()
64
- self.optional_tracing = optional_tracing
65
54
  self.user_hooks = user_hooks
66
55
 
67
- def _get_context_vars(self):
68
- """Get tracing context from context variables set by Paid.trace()."""
69
- external_customer_id = paid_external_customer_id_var.get()
70
- external_agent_id = paid_external_agent_id_var.get()
71
- token = paid_token_var.get()
72
- return external_customer_id, external_agent_id, token
73
-
74
- def _should_skip_tracing(self, external_customer_id: Optional[str], token: Optional[str]) -> bool:
75
- """Check if tracing should be skipped."""
76
- # Check if there's an active span (from Paid.trace())
77
- current_span = trace.get_current_span()
78
- if current_span == trace.INVALID_SPAN:
79
- if self.optional_tracing:
80
- logger.info(f"{self.__class__.__name__} No tracing, skipping LLM tracking.")
81
- return True
82
- raise RuntimeError("No OTEL span found. Make sure to call this method from Paid.trace().")
83
-
84
- if not (external_customer_id and token):
85
- if self.optional_tracing:
86
- logger.info(f"{self.__class__.__name__} No external_customer_id or token, skipping LLM tracking")
87
- return True
88
- raise RuntimeError(
89
- "Missing required tracing information: external_customer_id or token."
90
- " Make sure to call this method from Paid.trace()."
91
- )
92
- return False
93
-
94
56
  def _start_span(self, context, agent, hook_name) -> None:
95
57
  try:
96
- external_customer_id, external_agent_id, token = self._get_context_vars()
97
-
98
- # Skip tracing if required context is missing
99
- if self._should_skip_tracing(external_customer_id, token):
100
- return
58
+ tracer = get_paid_tracer()
101
59
 
102
60
  # Get model name from agent
103
61
  model_name = str(agent.model if agent.model else get_default_model())
104
62
 
105
63
  # Start span for this LLM call
106
- span = self.tracer.start_span(f"openai.agents.{hook_name}")
107
- logger.debug(f"{hook_name} : started span")
64
+ span = tracer.start_span(f"openai.agents.{hook_name}")
108
65
 
109
66
  # Set initial attributes
110
67
  attributes = {
111
68
  "gen_ai.system": "openai",
112
69
  "gen_ai.operation.name": f"{hook_name}",
113
- "external_customer_id": external_customer_id,
114
- "token": token,
115
70
  "gen_ai.request.model": model_name,
116
71
  }
117
- if external_agent_id:
118
- attributes["external_agent_id"] = external_agent_id
119
72
 
120
73
  span.set_attributes(attributes)
121
74
 
@@ -123,7 +76,6 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
123
76
  # This works across async boundaries without polluting user's context
124
77
  context_id = id(context)
125
78
  _paid_span_store[context_id] = span
126
- logger.debug(f"_start_span: Stored span for context ID {context_id}")
127
79
 
128
80
  except Exception as error:
129
81
  logger.error(f"Error while starting span in PaidAgentsHook.{hook_name}: {error}")
@@ -133,7 +85,6 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
133
85
  # Retrieve span from global dict using context object ID
134
86
  context_id = id(context)
135
87
  span = _paid_span_store.get(context_id)
136
- logger.debug(f"_end_span: Retrieved span for context ID {context_id}: {span}")
137
88
 
138
89
  if span:
139
90
  # Get usage data from the response
@@ -161,17 +112,13 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
161
112
  span.set_status(Status(StatusCode.ERROR, "No usage available"))
162
113
 
163
114
  span.end()
164
- logger.debug(f"{hook_name} : ended span")
165
115
 
166
116
  # Clean up from global dict
167
117
  del _paid_span_store[context_id]
168
- logger.debug(f"_end_span: Cleaned up span for context ID {context_id}")
169
- else:
170
- logger.warning(f"_end_span: No span found for context ID {context_id}")
171
118
 
172
119
  except Exception as error:
173
- logger.error(f"Error while ending span in PaidAgentsHook.{hook_name}_end: {error}")
174
120
  # Try to end span on error
121
+ logger.error(f"Error while ending span in PaidAgentsHook.{hook_name}: {error}")
175
122
  try:
176
123
  context_id = id(context)
177
124
  span = _paid_span_store.get(context_id)
@@ -181,26 +128,18 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
181
128
  span.end()
182
129
  del _paid_span_store[context_id]
183
130
  except:
184
- pass
131
+ logger.error(f"Failed to end span after error in PaidAgentsHook.{hook_name}")
185
132
 
186
133
  async def on_llm_start(self, context, agent, system_prompt, input_items) -> None:
187
- logger.debug(f"on_llm_start : context_usage : {getattr(context, 'usage', None)}")
188
-
189
134
  if self.user_hooks and hasattr(self.user_hooks, "on_llm_start"):
190
135
  await self.user_hooks.on_llm_start(context, agent, system_prompt, input_items)
191
136
 
192
137
  async def on_llm_end(self, context, agent, response) -> None:
193
- logger.debug(
194
- f"on_llm_end : context_usage : {getattr(context, 'usage', None)} : response_usage : {getattr(response, 'usage', None)}"
195
- )
196
-
197
138
  if self.user_hooks and hasattr(self.user_hooks, "on_llm_end"):
198
139
  await self.user_hooks.on_llm_end(context, agent, response)
199
140
 
200
141
  async def on_agent_start(self, context, agent) -> None:
201
142
  """Start a span for agent operations and call user hooks."""
202
- logger.debug(f"on_agent_start : context_usage : {getattr(context, 'usage', None)}")
203
-
204
143
  if self.user_hooks and hasattr(self.user_hooks, "on_agent_start"):
205
144
  await self.user_hooks.on_agent_start(context, agent)
206
145
 
@@ -208,26 +147,19 @@ class PaidOpenAIAgentsHook(RunHooks[Any]):
208
147
 
209
148
  async def on_agent_end(self, context, agent, output) -> None:
210
149
  """End the span for agent operations and call user hooks."""
211
- logger.debug(f"on_agent_end : context_usage : {getattr(context, 'usage', None)}")
212
-
213
150
  self._end_span(context, "on_agent")
214
151
 
215
152
  if self.user_hooks and hasattr(self.user_hooks, "on_agent_end"):
216
153
  await self.user_hooks.on_agent_end(context, agent, output)
217
154
 
218
155
  async def on_handoff(self, context, from_agent, to_agent) -> None:
219
- logger.debug(f"on_handoff : context_usage : {getattr(context, 'usage', None)}")
220
156
  if self.user_hooks and hasattr(self.user_hooks, "on_handoff"):
221
157
  await self.user_hooks.on_handoff(context, from_agent, to_agent)
222
158
 
223
159
  async def on_tool_start(self, context, agent, tool) -> None:
224
- logger.debug(f"on_tool_start : context_usage : {getattr(context, 'usage', None)}")
225
-
226
160
  if self.user_hooks and hasattr(self.user_hooks, "on_tool_start"):
227
161
  await self.user_hooks.on_tool_start(context, agent, tool)
228
162
 
229
163
  async def on_tool_end(self, context, agent, tool, result) -> None:
230
- logger.debug(f"on_tool_end : context_usage : {getattr(context, 'usage', None)}")
231
-
232
164
  if self.user_hooks and hasattr(self.user_hooks, "on_tool_end"):
233
165
  await self.user_hooks.on_tool_end(context, agent, tool, result)
paid/types/signal.py CHANGED
@@ -11,8 +11,25 @@ class Signal(UniversalBaseModel):
11
11
  agent_id: typing.Optional[str] = None
12
12
  external_agent_id: typing.Optional[str] = None
13
13
  customer_id: typing.Optional[str] = None
14
- external_customer_id: typing.Optional[str] = None
14
+ """
15
+ Deprecated. The external customer id. Use `external_customer_id` or `internal_customer_id` instead.
16
+ """
17
+
15
18
  data: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None
19
+ idempotency_key: typing.Optional[str] = pydantic.Field(default=None)
20
+ """
21
+ A unique key to ensure idempotent signal processing
22
+ """
23
+
24
+ internal_customer_id: typing.Optional[str] = pydantic.Field(default=None)
25
+ """
26
+ Paid's internal customer ID
27
+ """
28
+
29
+ external_customer_id: typing.Optional[str] = pydantic.Field(default=None)
30
+ """
31
+ Your system's customer ID
32
+ """
16
33
 
17
34
  if IS_PYDANTIC_V2:
18
35
  model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: paid-python
3
- Version: 0.0.5a39
3
+ Version: 0.1.0
4
4
  Summary:
5
5
  Requires-Python: >=3.9,<3.14
6
6
  Classifier: Intended Audience :: Developers
@@ -135,7 +135,7 @@ from paid.tracing import paid_tracing
135
135
 
136
136
  @paid_tracing("<external_customer_id>", external_agent_id="<optional_external_agent_id>")
137
137
  def some_agent_workflow(): # your function
138
- # Your logic - use any AI providers with Paid wrappers or send signals with Paid.signal().
138
+ # Your logic - use any AI providers with Paid wrappers or send signals with signal().
139
139
  # This function is typically an event processor that should lead to AI calls or events emitted as Paid signals
140
140
  ```
141
141
 
@@ -156,6 +156,7 @@ async with paid_tracing("customer_123", external_agent_id="agent_456"):
156
156
  ```
157
157
 
158
158
  Both approaches:
159
+
159
160
  - Initialize tracing using your API key you provided to the Paid client, falls back to `PAID_API_KEY` environment variable.
160
161
  - Handle both sync and async functions/code blocks
161
162
  - Gracefully fall back to normal execution if tracing fails
@@ -178,6 +179,7 @@ gemini (google-genai)
178
179
  ```
179
180
 
180
181
  Example usage:
182
+
181
183
  ```python
182
184
  from openai import OpenAI
183
185
  from paid.tracing import paid_tracing
@@ -261,31 +263,24 @@ paid_autoinstrument(libraries=["anthropic", "openai"])
261
263
 
262
264
  - Auto-instrumentation uses official OpenTelemetry instrumentors for each AI library
263
265
  - It automatically wraps library calls without requiring you to use Paid wrapper classes
264
- - Works seamlessly with `@paid_tracing()` decorator or `Paid.trace()` callback
266
+ - Works seamlessly with `@paid_tracing()` decorator or context manager
265
267
  - Costs are tracked in the same way as when using manual wrappers
266
268
  - Should be called once during application startup, typically before creating AI client instances
267
269
 
268
-
269
270
  ## Signaling via OTEL tracing
270
271
 
271
- A more reliable and user-friendly way to send signals is to send them via OTEL tracing.
272
- This allows you to send signals with less arguments and boilerplate as the information is available in the tracing context `Paid.trace()` or `@paid_tracing()`.
273
- The interface is `Paid.signal()`, which takes in signal name, optional data, and a flag that attaches costs from the same trace.
274
- `Paid.signal()` has to be called within a trace - meaning inside of a callback to `Paid.trace()`.
275
- In contrast to `Paid.usage.record_bulk()`, `Paid.signal()` is using OpenTelemetry to provide reliable delivery.
272
+ Signals allow you to emit events within your tracing context. They have access to all tracing information, so you need fewer arguments compared to manual API calls.
273
+ Use the `signal()` function which must be called within an active `@paid_tracing()` context (decorator or context manager).
276
274
 
277
275
  Here's an example of how to use it:
278
- ```python
279
- from paid import Paid
280
- from paid.tracing import paid_tracing
281
276
 
282
- # Initialize Paid SDK
283
- client = Paid(token="PAID_API_KEY")
277
+ ```python
278
+ from paid.tracing import paid_tracing, signal
284
279
 
285
- @paid_tracing("your_external_customer_id", "your_external_agent_id") # external_agent_id is necessary for sending signals
280
+ @paid_tracing("your_external_customer_id", "your_external_agent_id")
286
281
  def do_work():
287
282
  # ...do some work...
288
- client.signal(
283
+ signal(
289
284
  event_name="<your_signal_name>",
290
285
  data={ } # optional data (ex. manual cost tracking data)
291
286
  )
@@ -293,27 +288,21 @@ def do_work():
293
288
  do_work()
294
289
  ```
295
290
 
296
- Same, but using callback to specify the function to trace:
297
- ```python
298
- from paid import Paid
291
+ Same approach with context manager:
299
292
 
300
- # Initialize Paid SDK
301
- client = Paid(token="PAID_API_KEY")
302
-
303
- # Initialize tracing, must be after initializing Paid SDK
304
- client.initialize_tracing()
293
+ ```python
294
+ from paid.tracing import paid_tracing, signal
305
295
 
306
296
  def do_work():
307
297
  # ...do some work...
308
- client.signal(
298
+ signal(
309
299
  event_name="<your_signal_name>",
310
300
  data={ } # optional data (ex. manual cost tracking data)
311
301
  )
312
302
 
313
- # Finally, capture the traces!
314
- client.trace(external_customer_id = "<your_external_customer_id>",
315
- external_agent_id = "<your_external_agent_id>", # external_agent_id is required for signals
316
- fn = lambda: do_work())
303
+ # Use context manager instead
304
+ with paid_tracing("your_external_customer_id", "your_external_agent_id"):
305
+ do_work()
317
306
  ```
318
307
 
319
308
  ### Signal-costs - Attaching cost traces to a signal
@@ -325,17 +314,13 @@ as the wrappers and hooks that recorded those costs.
325
314
  This will look something like this:
326
315
 
327
316
  ```python
328
- from paid import Paid
329
- from paid.tracing import paid_tracing
330
-
331
- # Initialize Paid SDK
332
- client = Paid(token="PAID_API_KEY")
317
+ from paid.tracing import paid_tracing, signal
333
318
 
334
- @paid_tracing("your_external_customer_id", "your_external_agent_id") # external_agent_id is necessary for sending signals
319
+ @paid_tracing("your_external_customer_id", "your_external_agent_id")
335
320
  def do_work():
336
321
  # ... your workflow logic
337
322
  # ... your AI calls made through Paid wrappers or hooks
338
- client.signal(
323
+ signal(
339
324
  event_name="<your_signal_name>",
340
325
  data={ }, # optional data (ex. manual cost tracking data)
341
326
  enable_cost_tracing=True, # set this flag to associate it with costs
@@ -353,20 +338,17 @@ Then, all of the costs traced in @paid_tracing() context are related to that sig
353
338
  Sometimes your agent workflow cannot fit into a single traceable function like above,
354
339
  because it has to be disjoint for whatever reason. It could even be running across different machines.
355
340
 
356
- For such cases, you can pass a tracing token directly to `@paid_tracing()` or `Paid.trace()` to link distributed traces together.
341
+ For such cases, you can pass a tracing token directly to `@paid_tracing()` or context manager to link distributed traces together.
357
342
 
358
343
  #### Using `tracing_token` parameter (Recommended)
359
344
 
360
- The simplest way to implement distributed tracing is to pass the token directly to the decorator or trace function:
345
+ The simplest way to implement distributed tracing is to pass the token directly to the decorator or context manager:
361
346
 
362
347
  ```python
363
- from paid import Paid
364
- from paid.tracing import paid_tracing, generate_tracing_token
348
+ from paid.tracing import paid_tracing, signal, generate_tracing_token
365
349
  from paid.tracing.wrappers.openai import PaidOpenAI
366
350
  from openai import OpenAI
367
351
 
368
- # Initialize
369
- client = Paid(token="<PAID_API_KEY>")
370
352
  openai_client = PaidOpenAI(OpenAI(api_key="<OPENAI_API_KEY>"))
371
353
 
372
354
  # Process 1: Generate token and do initial work
@@ -384,7 +366,7 @@ def process_part_1():
384
366
  messages=[{"role": "user", "content": "Analyze data"}]
385
367
  )
386
368
  # Signal without cost tracing
387
- client.signal("part_1_complete", enable_cost_tracing=False)
369
+ signal("part_1_complete", enable_cost_tracing=False)
388
370
 
389
371
  process_part_1()
390
372
 
@@ -399,167 +381,44 @@ def process_part_2():
399
381
  messages=[{"role": "user", "content": "Generate response"}]
400
382
  )
401
383
  # Signal WITH cost tracing - links all costs from both processes
402
- client.signal("workflow_complete", enable_cost_tracing=True)
384
+ signal("workflow_complete", enable_cost_tracing=True)
403
385
 
404
386
  process_part_2()
405
387
  # No cleanup needed - token is scoped to the decorated function
406
388
  ```
407
389
 
408
- Using `Paid.trace()` instead of decorator:
390
+ Using context manager instead of decorator:
409
391
 
410
392
  ```python
411
- from paid import Paid
412
- from paid.tracing import generate_tracing_token
393
+ from paid.tracing import paid_tracing, signal, generate_tracing_token
413
394
  from paid.tracing.wrappers.openai import PaidOpenAI
414
395
  from openai import OpenAI
415
396
 
416
397
  # Initialize
417
- client = Paid(token="<PAID_API_KEY>")
418
- client.initialize_tracing()
419
398
  openai_client = PaidOpenAI(OpenAI(api_key="<OPENAI_API_KEY>"))
420
399
 
421
- # Process 1: Generate and use token
400
+ # Process 1: Generate token and do initial work
422
401
  token = generate_tracing_token()
423
402
  save_to_storage("workflow_123", token)
424
403
 
425
- def process_part_1():
404
+ with paid_tracing("customer_123", external_agent_id="agent_123", tracing_token=token):
426
405
  response = openai_client.chat.completions.create(
427
406
  model="gpt-4",
428
407
  messages=[{"role": "user", "content": "Analyze data"}]
429
408
  )
430
- client.signal("part_1_complete", enable_cost_tracing=False)
431
-
432
- client.trace(
433
- external_customer_id="customer_123",
434
- external_agent_id="agent_123",
435
- tracing_token=token,
436
- fn=lambda: process_part_1()
437
- )
409
+ signal("part_1_complete", enable_cost_tracing=False)
438
410
 
439
411
  # Process 2: Retrieve and use the same token
440
412
  token = load_from_storage("workflow_123")
441
413
 
442
- def process_part_2():
414
+ with paid_tracing("customer_123", external_agent_id="agent_123", tracing_token=token):
443
415
  response = openai_client.chat.completions.create(
444
416
  model="gpt-4",
445
417
  messages=[{"role": "user", "content": "Generate response"}]
446
418
  )
447
- client.signal("workflow_complete", enable_cost_tracing=True)
448
-
449
- client.trace(
450
- external_customer_id="customer_123",
451
- external_agent_id="agent_123",
452
- tracing_token=token,
453
- fn=lambda: process_part_2()
454
- )
455
- ```
456
-
457
- #### Alternative: Using global context (Advanced)
458
-
459
- For more complex scenarios where you need to set the tracing context globally, you can use these functions:
460
-
461
- ```python
462
- from paid.tracing import (
463
- generate_tracing_token,
464
- generate_and_set_tracing_token,
465
- set_tracing_token,
466
- unset_tracing_token
467
- )
468
-
469
- def generate_tracing_token() -> int:
470
- """
471
- Generates and returns a tracing token without setting it in the tracing context.
472
- Useful when you only want to store or send a tracing token somewhere else
473
- without immediately activating it.
474
-
475
- Returns:
476
- int: The tracing token (OpenTelemetry trace ID)
477
- """
478
-
479
- def generate_and_set_tracing_token() -> int:
480
- """
481
- This function returns tracing token and attaches it to all consequent
482
- Paid.trace() or @paid_tracing tracing contexts. So all the costs and signals that share this
483
- tracing context are associated with each other.
484
-
485
- To stop associating the traces one can either call
486
- generate_and_set_tracing_token() once again or call unset_tracing_token().
487
- The former is suitable if you still want to trace but in a fresh
488
- context, and the latter will go back to unique traces per Paid.trace().
489
-
490
- Returns:
491
- int: The tracing token (OpenTelemetry trace ID)
492
- """
493
-
494
- def set_tracing_token(token: int):
495
- """
496
- Sets tracing token. Provided token should come from generate_and_set_tracing_token()
497
- or generate_tracing_token(). Once set, the consequent traces Paid.trace() or
498
- @paid_tracing() will be related to each other.
499
-
500
- Args:
501
- token (int): A tracing token from generate_and_set_tracing_token() or generate_tracing_token()
502
- """
503
-
504
- def unset_tracing_token():
505
- """
506
- Unsets the token previously set by generate_and_set_tracing_token()
507
- or by set_tracing_token(token). Does nothing if the token was never set.
508
- """
419
+ signal("workflow_complete", enable_cost_tracing=True)
509
420
  ```
510
421
 
511
- Example using global context:
512
-
513
- ```python
514
- from paid import Paid
515
- from paid.tracing import paid_tracing, generate_and_set_tracing_token, set_tracing_token, unset_tracing_token
516
- from paid.tracing.wrappers.openai import PaidOpenAI
517
- from openai import OpenAI
518
-
519
- # Initialize
520
- client = Paid(token="<PAID_API_KEY>")
521
- openai_client = PaidOpenAI(OpenAI(api_key="<OPENAI_API_KEY>"))
522
-
523
- # Process 1: Generate token and do initial work
524
- token = generate_and_set_tracing_token()
525
- print(f"Tracing token: {token}")
526
-
527
- # Store token for other processes (e.g., in Redis, database, message queue)
528
- save_to_storage("workflow_123", token)
529
-
530
- @paid_tracing("customer_123", external_agent_id="agent_123")
531
- def process_part_1():
532
- # AI calls here will be traced
533
- response = openai_client.chat.completions.create(
534
- model="gpt-4",
535
- messages=[{"role": "user", "content": "Analyze data"}]
536
- )
537
- # Signal without cost tracing
538
- client.signal("part_1_complete", enable_cost_tracing=False)
539
-
540
- process_part_1()
541
-
542
- # Process 2 (different machine/process): Retrieve and use token
543
- token = load_from_storage("workflow_123")
544
- set_tracing_token(token)
545
-
546
- @paid_tracing("customer_123", external_agent_id="agent_123")
547
- def process_part_2():
548
- # AI calls here will be linked to the same trace
549
- response = openai_client.chat.completions.create(
550
- model="gpt-4",
551
- messages=[{"role": "user", "content": "Generate response"}]
552
- )
553
- # Signal WITH cost tracing - links all costs from both processes
554
- client.signal("workflow_complete", enable_cost_tracing=True)
555
-
556
- process_part_2()
557
-
558
- # Clean up
559
- unset_tracing_token()
560
- ```
561
-
562
-
563
422
  ## Manual Cost Tracking
564
423
 
565
424
  If you would prefer to not use Paid to track your costs automatically but you want to send us the costs yourself,
@@ -573,7 +432,7 @@ client = Paid(token="<PAID_API_KEY>")
573
432
  signal = Signal(
574
433
  event_name="<your_signal_name>",
575
434
  agent_id="<your_agent_id>",
576
- customer_id="<your_external_customer_id>",
435
+ external_customer_id="<your_external_customer_id>",
577
436
  data = {
578
437
  "costData": {
579
438
  "vendor": "<any_vendor_name>", # can be anything, traces are grouped by vendors in the UI
@@ -592,16 +451,12 @@ client.usage.record_bulk(signals=[signal])
592
451
  Alternatively the same `costData` payload can be passed to OTLP signaling mechanism:
593
452
 
594
453
  ```python
595
- from paid import Paid
596
- from paid.tracing import paid_tracing
454
+ from paid.tracing import paid_tracing, signal
597
455
 
598
- # Initialize Paid SDK
599
- client = Paid(token="PAID_API_KEY")
600
-
601
- @paid_tracing("your_external_customer_id", "your_external_agent_id") # external_agent_id is required for sending signals
456
+ @paid_tracing("your_external_customer_id", "your_external_agent_id")
602
457
  def do_work():
603
458
  # ...do some work...
604
- client.signal(
459
+ signal(
605
460
  event_name="<your_signal_name>",
606
461
  data={
607
462
  "costData": {
@@ -630,7 +485,7 @@ client = Paid(token="<PAID_API_KEY>")
630
485
  signal = Signal(
631
486
  event_name="<your_signal_name>",
632
487
  agent_id="<your_agent_id>",
633
- customer_id="<your_external_customer_id>",
488
+ external_customer_id="<your_external_customer_id>",
634
489
  data = {
635
490
  "costData": {
636
491
  "vendor": "<any_vendor_name>", # can be anything, traces are grouped by vendors in the UI
@@ -650,16 +505,12 @@ client.usage.record_bulk(signals=[signal])
650
505
  Same but via OTEL signaling:
651
506
 
652
507
  ```python
653
- from paid import Paid
654
- from paid.tracing import paid_tracing
508
+ from paid.tracing import paid_tracing, signal
655
509
 
656
- # Initialize Paid SDK
657
- client = Paid(token="PAID_API_KEY")
658
-
659
- @paid_tracing("your_external_customer_id", "your_external_agent_id") # external_agent_id is required for sending signals
510
+ @paid_tracing("your_external_customer_id", "your_external_agent_id")
660
511
  def do_work():
661
512
  # ...do some work...
662
- client.signal(
513
+ signal(
663
514
  event_name="<your_signal_name>",
664
515
  data={
665
516
  "costData": {
@@ -723,15 +574,13 @@ await generate_image()
723
574
 
724
575
  ### Async Signaling
725
576
 
726
- The `signal()` method works seamlessly in async contexts:
577
+ The `signal()` function works seamlessly in async contexts:
727
578
 
728
579
  ```python
729
- from paid import AsyncPaid
730
- from paid.tracing import paid_tracing
580
+ from paid.tracing import paid_tracing, signal
731
581
  from paid.tracing.wrappers.openai import PaidAsyncOpenAI
732
582
  from openai import AsyncOpenAI
733
583
 
734
- client = AsyncPaid(token="PAID_API_KEY")
735
584
  openai_client = PaidAsyncOpenAI(AsyncOpenAI(api_key="<OPENAI_API_KEY>"))
736
585
 
737
586
  @paid_tracing("your_external_customer_id", "your_external_agent_id")
@@ -742,8 +591,8 @@ async def do_work():
742
591
  messages=[{"role": "user", "content": "Hello!"}]
743
592
  )
744
593
 
745
- # Send signal (synchronous call within async function)
746
- client.signal(
594
+ # Send signal (works in async context)
595
+ signal(
747
596
  event_name="<your_signal_name>",
748
597
  enable_cost_tracing=True # Associate with traced costs
749
598
  )