paygent-sdk 3.0.0__py3-none-any.whl → 4.0.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.
paygent_sdk/__init__.py CHANGED
@@ -8,8 +8,10 @@ For the Go SDK equivalent, see: https://github.com/paygent/paygent-sdk-go
8
8
 
9
9
  from .client import Client
10
10
  from .models import (
11
- UsageData, UsageDataWithStrings, APIRequest, ModelPricing, MODEL_PRICING,
12
- SttUsageData, TtsUsageData, SttModelPricing, TtsModelPricing
11
+ UsageData, UsageDataWithStrings, APIRequest,
12
+ SttUsageData, TtsUsageData,
13
+ RawUsageData, SendUsageV2Response, CostBreakdown,
14
+ Customer, CustomerCreateOrGetRequest
13
15
  )
14
16
  from .voice_client import send_stt_usage, send_tts_usage # Import to attach methods to Client
15
17
 
@@ -50,14 +52,17 @@ __all__ = [
50
52
  "UsageData",
51
53
  "UsageDataWithStrings",
52
54
  "APIRequest",
53
- "ModelPricing",
54
- "MODEL_PRICING",
55
55
 
56
56
  # Voice data models
57
57
  "SttUsageData",
58
58
  "TtsUsageData",
59
- "SttModelPricing",
60
- "TtsModelPricing",
59
+
60
+ # New V2 & Customer models
61
+ "RawUsageData",
62
+ "SendUsageV2Response",
63
+ "CostBreakdown",
64
+ "Customer",
65
+ "CustomerCreateOrGetRequest",
61
66
 
62
67
  # Wrappers
63
68
  "PaygentOpenAI",
paygent_sdk/client.py CHANGED
@@ -17,7 +17,16 @@ try:
17
17
  except ImportError:
18
18
  tiktoken = None
19
19
 
20
- from .models import UsageData, UsageDataWithStrings, APIRequest, ModelPricing, MODEL_PRICING
20
+ from .models import (
21
+ UsageData,
22
+ UsageDataWithStrings,
23
+ APIRequest,
24
+ RawUsageData,
25
+ SendUsageV2Response,
26
+ CostBreakdown,
27
+ Customer,
28
+ CustomerCreateOrGetRequest,
29
+ )
21
30
 
22
31
 
23
32
  class Client:
@@ -79,52 +88,6 @@ class Client:
79
88
  """
80
89
  return cls(api_key)
81
90
 
82
- def _calculate_cost(self, model: str, usage_data: UsageData) -> float:
83
- """
84
- Calculate the cost based on model and usage data.
85
-
86
- Args:
87
- model: The AI model name
88
- usage_data: Usage data containing token counts
89
-
90
- Returns:
91
- Calculated cost in USD
92
- """
93
- pricing = MODEL_PRICING.get(model)
94
- if not pricing:
95
- self.logger.warning(f"Unknown model '{model}', using default pricing")
96
- # Use default pricing for unknown models (per 1000 tokens)
97
- pricing = ModelPricing(
98
- prompt_tokens_cost=0.0001, # $0.10 per 1000 tokens
99
- completion_tokens_cost=0.0001 # $0.10 per 1000 tokens
100
- )
101
-
102
- # Calculate cost per 1000 tokens
103
- prompt_cost = (usage_data.prompt_tokens / 1000.0) * pricing.prompt_tokens_cost
104
-
105
- # Handle cached tokens: if model doesn't support caching (cached_tokens_cost is None),
106
- # bill cached tokens at regular prompt token rate
107
- cached_cost = 0.0
108
- if usage_data.cached_tokens and usage_data.cached_tokens > 0:
109
- if pricing.cached_tokens_cost is not None:
110
- # Model supports caching - use cached token price
111
- cached_cost = (usage_data.cached_tokens / 1000.0) * pricing.cached_tokens_cost
112
- else:
113
- # Model doesn't support caching - bill at prompt token rate
114
- cached_cost = (usage_data.cached_tokens / 1000.0) * pricing.prompt_tokens_cost
115
-
116
- completion_cost = (usage_data.completion_tokens / 1000.0) * pricing.completion_tokens_cost
117
- total_cost = prompt_cost + cached_cost + completion_cost
118
-
119
- self.logger.debug(
120
- f"Cost calculation for model '{model}': "
121
- f"prompt_tokens={usage_data.prompt_tokens} ({prompt_cost:.6f}), "
122
- f"cached_tokens={usage_data.cached_tokens or 0} ({cached_cost:.6f}), "
123
- f"completion_tokens={usage_data.completion_tokens} ({completion_cost:.6f}), "
124
- f"total={total_cost:.6f}"
125
- )
126
-
127
- return total_cost
128
91
 
129
92
  def send_usage(
130
93
  self,
@@ -174,67 +137,6 @@ class Client:
174
137
  # Cost calculated (no logging for performance)
175
138
 
176
139
  # Prepare API request
177
- api_request = APIRequest(
178
- agent_id=agent_id,
179
- customer_id=customer_id,
180
- indicator=indicator,
181
- amount=cost
182
- )
183
-
184
- # Prepare request data
185
- request_data = {
186
- "agentId": api_request.agent_id,
187
- "customerId": api_request.customer_id,
188
- "indicator": api_request.indicator,
189
- "amount": api_request.amount,
190
- "inputToken": regular_prompt_tokens, # Send non-cached tokens
191
- "cachedToken": cached_tokens, # Send cached tokens separately
192
- "outputToken": usage_data.completion_tokens,
193
- "model": usage_data.model,
194
- "serviceProvider": usage_data.service_provider
195
- }
196
-
197
- self.logger.debug(f"API request body: {json.dumps(request_data)}")
198
-
199
- # Create HTTP request
200
- url = urljoin(self.base_url, "/api/v1/usage")
201
-
202
- headers = {
203
- "Content-Type": "application/json",
204
- "paygent-api-key": self.api_key
205
- }
206
-
207
- self.logger.debug(f"Making HTTP POST request to: {url}")
208
-
209
- try:
210
- # Make HTTP request
211
- response = self.session.post(
212
- url,
213
- json=request_data,
214
- headers=headers,
215
- timeout=30
216
- )
217
-
218
- self.logger.debug(
219
- f"API response status: {response.status_code}, "
220
- f"body: {response.text}"
221
- )
222
-
223
- # Check response status
224
- if 200 <= response.status_code < 300:
225
- # Success - no logging to minimize verbosity
226
- return
227
-
228
- # Handle error response
229
- self.logger.error(
230
- f"API request failed with status {response.status_code}: {response.text}"
231
- )
232
- response.raise_for_status()
233
-
234
- except requests.RequestException as e:
235
- self.logger.error(f"HTTP request failed: {e}")
236
- raise
237
-
238
140
  def set_log_level(self, level: int) -> None:
239
141
  """
240
142
  Set the logging level for the client.
@@ -339,38 +241,34 @@ class Client:
339
241
  word_count = len(text.split())
340
242
  return int(word_count * 1.3)
341
243
 
342
- def _calculate_cost_from_strings(self, model: str, usage_data: UsageDataWithStrings) -> float:
244
+ def send_usage(
245
+ self,
246
+ agent_id: str,
247
+ customer_id: str,
248
+ indicator: str,
249
+ usage_data: UsageData
250
+ ) -> None:
343
251
  """
344
- Calculate the cost based on model and usage data with strings.
252
+ Send usage data to the Paygent API.
253
+ This legacy method now internally uses the V2 API for server-side pricing.
345
254
 
346
255
  Args:
347
- model: The AI model name
348
- usage_data: Usage data containing prompt and output strings
256
+ agent_id: Unique identifier for the agent
257
+ customer_id: Unique identifier for the customer
258
+ indicator: Indicator for the usage event
259
+ usage_data: Usage data containing model and token information
349
260
 
350
- Returns:
351
- Calculated cost in USD
261
+ Raises:
262
+ requests.RequestException: If the HTTP request fails
352
263
  """
353
- # Count tokens
354
- prompt_tokens = self._get_token_count(model, usage_data.prompt_string)
355
- completion_tokens = self._get_token_count(model, usage_data.output_string)
356
- total_tokens = prompt_tokens + completion_tokens
357
-
358
- self.logger.debug(
359
- f"Token counting for model '{model}': "
360
- f"prompt_tokens={prompt_tokens}, completion_tokens={completion_tokens}, "
361
- f"total_tokens={total_tokens}"
362
- )
363
-
364
- # Create UsageData for cost calculation
365
- usage_data_obj = UsageData(
366
- service_provider=usage_data.service_provider,
367
- model=model,
368
- prompt_tokens=prompt_tokens,
369
- completion_tokens=completion_tokens,
370
- total_tokens=total_tokens
264
+ raw_usage = RawUsageData(
265
+ provider=usage_data.service_provider,
266
+ model=usage_data.model,
267
+ input_tokens=usage_data.prompt_tokens,
268
+ output_tokens=usage_data.completion_tokens,
269
+ cached_tokens=usage_data.cached_tokens
371
270
  )
372
-
373
- return self._calculate_cost(model, usage_data_obj)
271
+ self.send_usage_v2(agent_id, customer_id, indicator, raw_usage)
374
272
 
375
273
  def send_usage_with_token_string(
376
274
  self,
@@ -381,7 +279,8 @@ class Client:
381
279
  ) -> None:
382
280
  """
383
281
  Send usage data to the Paygent API using prompt and output strings.
384
- The function automatically counts tokens using proper tokenizers for each model provider and calculates costs.
282
+ The function automatically counts tokens using proper tokenizers for each model provider.
283
+ This method now internally uses the V2 API for server-side pricing.
385
284
 
386
285
  Args:
387
286
  agent_id: Unique identifier for the agent
@@ -393,54 +292,73 @@ class Client:
393
292
  requests.RequestException: If the HTTP request fails
394
293
  ValueError: If the usage data is invalid
395
294
  """
396
- # Removed verbose logging - only log errors
397
- # Calculate cost from strings
398
- try:
399
- cost = self._calculate_cost_from_strings(usage_data.model, usage_data)
400
- except Exception as e:
401
- self.logger.error(f"Failed to calculate cost from strings: {e}")
402
- raise ValueError(f"Failed to calculate cost from strings: {e}") from e
403
-
404
- # Cost calculated from strings (no logging for performance)
405
-
406
295
  # Calculate token counts for API request
407
296
  prompt_tokens = self._get_token_count(usage_data.model, usage_data.prompt_string)
408
297
  completion_tokens = self._get_token_count(usage_data.model, usage_data.output_string)
409
298
 
410
- # Prepare API request
411
- api_request = APIRequest(
412
- agent_id=agent_id,
413
- customer_id=customer_id,
414
- indicator=indicator,
415
- amount=cost
299
+ raw_usage = RawUsageData(
300
+ provider=usage_data.service_provider,
301
+ model=usage_data.model,
302
+ input_tokens=prompt_tokens,
303
+ output_tokens=completion_tokens
416
304
  )
417
-
305
+ self.send_usage_v2(agent_id, customer_id, indicator, raw_usage)
306
+
307
+ def send_usage_v2(
308
+ self,
309
+ agent_id: str,
310
+ customer_id: str,
311
+ indicator: str,
312
+ raw_usage: RawUsageData
313
+ ) -> SendUsageV2Response:
314
+ """
315
+ Send usage data to the Paygent API v2 (backend-calculated pricing).
316
+
317
+ Args:
318
+ agent_id: Unique identifier for the agent
319
+ customer_id: Unique identifier for the customer
320
+ indicator: Indicator for the usage event
321
+ raw_usage: Raw usage data including model, provider, tokens, etc.
322
+
323
+ Returns:
324
+ SendUsageV2Response containing cost breakdown and model metadata
325
+
326
+ Raises:
327
+ requests.RequestException: If the HTTP request fails
328
+ """
329
+ self.logger.debug(f"Sending V2 usage data for model: {raw_usage.model}")
330
+
418
331
  # Prepare request data
419
332
  request_data = {
420
- "agentId": api_request.agent_id,
421
- "customerId": api_request.customer_id,
422
- "indicator": api_request.indicator,
423
- "amount": api_request.amount,
424
- "inputToken": prompt_tokens,
425
- "outputToken": completion_tokens,
426
- "model": usage_data.model,
427
- "serviceProvider": usage_data.service_provider
333
+ "agentId": agent_id,
334
+ "customerId": customer_id,
335
+ "indicator": indicator,
336
+ "rawUsage": {
337
+ "provider": raw_usage.provider,
338
+ "model": raw_usage.model,
339
+ }
428
340
  }
429
-
430
- self.logger.debug(f"API request body: {json.dumps(request_data)}")
431
-
432
- # Create HTTP request
433
- url = urljoin(self.base_url, "/api/v1/usage")
434
341
 
342
+ # Add optional fields
343
+ if raw_usage.input_tokens is not None:
344
+ request_data["rawUsage"]["inputTokens"] = raw_usage.input_tokens
345
+ if raw_usage.output_tokens is not None:
346
+ request_data["rawUsage"]["outputTokens"] = raw_usage.output_tokens
347
+ if raw_usage.cached_tokens is not None:
348
+ request_data["rawUsage"]["cachedTokens"] = raw_usage.cached_tokens
349
+ if raw_usage.audio_duration is not None:
350
+ request_data["rawUsage"]["audioDuration"] = raw_usage.audio_duration
351
+ if raw_usage.character_count is not None:
352
+ request_data["rawUsage"]["characterCount"] = raw_usage.character_count
353
+
354
+ # Create HTTP request
355
+ url = urljoin(self.base_url, "/api/v2/usage")
435
356
  headers = {
436
357
  "Content-Type": "application/json",
437
358
  "paygent-api-key": self.api_key
438
359
  }
439
-
440
- self.logger.debug(f"Making HTTP POST request to: {url}")
441
-
360
+
442
361
  try:
443
- # Make HTTP request
444
362
  response = self.session.post(
445
363
  url,
446
364
  json=request_data,
@@ -449,21 +367,127 @@ class Client:
449
367
  )
450
368
 
451
369
  self.logger.debug(
452
- f"API response status: {response.status_code}, "
370
+ f"V2 API response status: {response.status_code}, "
453
371
  f"body: {response.text}"
454
372
  )
373
+
374
+ response.raise_for_status()
375
+
376
+ # Parse response
377
+ response_data = response.json()
378
+ self.logger.info("V2 usage data sent successfully")
379
+
380
+ # Parse breakdown
381
+ breakdown_data = response_data.get("breakdown", {})
382
+ breakdown = CostBreakdown(
383
+ input_cost=breakdown_data.get("inputCost", 0.0),
384
+ output_cost=breakdown_data.get("outputCost", 0.0),
385
+ cached_cost=breakdown_data.get("cachedCost", 0.0),
386
+ audio_cost=breakdown_data.get("audioCost", 0.0),
387
+ total_cost=breakdown_data.get("totalCost", 0.0)
388
+ )
389
+
390
+ return SendUsageV2Response(
391
+ cp_data_id=response_data.get("cpDataId", ""),
392
+ calculated_cost=response_data.get("calculatedCost", 0.0),
393
+ breakdown=breakdown,
394
+ model_metadata=response_data.get("modelMetadata", {})
395
+ )
396
+
397
+ except requests.RequestException as e:
398
+ self.logger.error(f"Failed to send V2 usage data: {e}")
399
+ raise
455
400
 
456
- # Check response status
457
- if 200 <= response.status_code < 300:
458
- # Success - no logging to minimize verbosity
459
- return
460
-
461
- # Handle error response
462
- self.logger.error(
463
- f"API request failed with status {response.status_code}: {response.text}"
401
+ def create_or_get_customer(self, request: CustomerCreateOrGetRequest) -> Customer:
402
+ """
403
+ Create a customer if they don't exist, or return the existing customer.
404
+ This method is idempotent - calling it multiple times with the same external_id will return the same customer.
405
+
406
+ Args:
407
+ request: Customer creation request with required name and external_id
408
+
409
+ Returns:
410
+ The created or existing customer
411
+
412
+ Raises:
413
+ ValueError: If validation fails
414
+ requests.RequestException: If the HTTP request fails
415
+ """
416
+ # Validate required fields
417
+ if not request.name or request.name.strip() == '':
418
+ self.logger.error('Validation failed: Customer name is required')
419
+ raise ValueError('Customer name is required')
420
+
421
+ if not request.external_id or request.external_id.strip() == '':
422
+ self.logger.error('Validation failed: Customer external_id is required')
423
+ raise ValueError('Customer external_id is required')
424
+
425
+ self.logger.debug(f"Creating or getting customer with external_id: {request.external_id}")
426
+
427
+ try:
428
+ # Prepare request data
429
+ request_data = {
430
+ "name": request.name,
431
+ "externalId": request.external_id,
432
+ }
433
+
434
+ # Add optional fields
435
+ if request.email:
436
+ request_data["email"] = request.email
437
+ if request.website:
438
+ request_data["website"] = request.website
439
+ if request.phone:
440
+ request_data["phone"] = request.phone
441
+ if request.address_line1:
442
+ request_data["addressLine1"] = request.address_line1
443
+ if request.address_line2:
444
+ request_data["addressLine2"] = request.address_line2
445
+ if request.city:
446
+ request_data["city"] = request.city
447
+ if request.state:
448
+ request_data["state"] = request.state
449
+ if request.zip_code:
450
+ request_data["zipCode"] = request.zip_code
451
+ if request.country:
452
+ request_data["country"] = request.country
453
+
454
+ # Call create-or-get endpoint
455
+ url = urljoin(self.base_url, "/api/v1/customers/create-or-get")
456
+ headers = {
457
+ "Content-Type": "application/json",
458
+ "paygent-api-key": self.api_key
459
+ }
460
+
461
+ response = self.session.post(
462
+ url,
463
+ json=request_data,
464
+ headers=headers,
465
+ timeout=30
464
466
  )
467
+
465
468
  response.raise_for_status()
466
-
469
+
470
+ # Parse response
471
+ response_data = response.json()
472
+ self.logger.info(f"Customer created/retrieved successfully with external_id: {request.external_id}")
473
+
474
+ return Customer(
475
+ id=response_data.get("id", ""),
476
+ external_id=response_data.get("externalId", ""),
477
+ name=response_data.get("name", ""),
478
+ email=response_data.get("email"),
479
+ website=response_data.get("website"),
480
+ phone=response_data.get("phone"),
481
+ address_line1=response_data.get("addressLine1"),
482
+ address_line2=response_data.get("addressLine2"),
483
+ city=response_data.get("city"),
484
+ state=response_data.get("state"),
485
+ zip_code=response_data.get("zipCode"),
486
+ country=response_data.get("country"),
487
+ created_at=response_data.get("createdAt"),
488
+ updated_at=response_data.get("updatedAt")
489
+ )
490
+
467
491
  except requests.RequestException as e:
468
- self.logger.error(f"HTTP request failed: {e}")
492
+ self.logger.error(f"Failed to create or get customer: {e}")
469
493
  raise