pytokencalc 0.7.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.
@@ -0,0 +1,111 @@
1
+ """
2
+ PyTokenCalc v0.7: Multi-Provider LLM Token Counting & Cost Calculator
3
+
4
+ Unified token counting and cost calculation across 20+ cloud providers and 10+ open-source APIs.
5
+ The token counting core for OpenAnchor cost optimization middleware.
6
+
7
+ v0.6+ Multi-Provider Token Models:
8
+ - Provider-specific token counting (Claude simple, GPT-4o dual, Gemini character-based, etc.)
9
+ - Local tokenizers (tiktoken, HF transformers) - 60% of models
10
+ - Cloud APIs with aggressive caching (Anthropic, Google)
11
+ - Vision/multimodal support (images, PDFs)
12
+
13
+ v0.5 Core Features (Backwards Compatible):
14
+ - CostCalculator: Calculate cost for any provider/model/tokens
15
+ - CostDatabase: Track operations and aggregate costs
16
+ - PricingManager: Provider pricing + daily updates
17
+ - Budget Enforcement: Hard limits to prevent cost overruns
18
+
19
+ v0.7+ New Features:
20
+ - TokenCounterRegistry: Unified token counting interface
21
+ - Intelligent routing: auto-detect tokenizer per model
22
+ - In-memory caching: reduce API calls 70-80%
23
+ - Vision tokens: images, PDFs, multimodal
24
+
25
+ Core Team:
26
+ - PyTokenCalc: Token counting & cost calculation
27
+ - OpenAnchor: Cost optimization middleware
28
+ - PrismNote: OSS data science notebook
29
+
30
+ Repository: https://github.com/Mullassery/PyTokenCalc
31
+ """
32
+
33
+ __version__ = "0.7.0"
34
+ __author__ = "Georgi Mammen Mullassery"
35
+
36
+ # Core cost calculation classes (v0.5 and v0.6)
37
+ from .cost_calculator import CostCalculator, CostCalculatorV6
38
+ from .cost_model import Cost, ProviderType
39
+ from .cost_models import (
40
+ UsageData,
41
+ CostModel,
42
+ CostModelRegistry,
43
+ ClaudeTokenModel,
44
+ GPT4oTokenModel,
45
+ GeminiCharacterModel,
46
+ GroqSpeedTieredModel,
47
+ DeepInfraTokenModel,
48
+ TogetherAITokenModel,
49
+ )
50
+ from .pricing_manager import PricingManager
51
+ from .database import DatabaseManager
52
+ from .persistence import CostDatabase
53
+
54
+ # Tokenizers (v0.7+: Multi-provider token counting)
55
+ try:
56
+ from .tokenizers import (
57
+ TokenCounter,
58
+ TokenCountResult,
59
+ TokenCounterRegistry,
60
+ TokenCounterCache,
61
+ )
62
+ TOKENIZERS_AVAILABLE = True
63
+ except ImportError:
64
+ TOKENIZERS_AVAILABLE = False
65
+
66
+ # Safety feature: Hard budget enforcement
67
+ from ._budget_enforcement import (
68
+ BudgetEnforcer,
69
+ BudgetLimit,
70
+ BudgetStatus,
71
+ BudgetPeriod,
72
+ EnforcementAction,
73
+ BudgetExceededError,
74
+ get_global_enforcer,
75
+ set_budget_limit,
76
+ )
77
+
78
+ __all__ = [
79
+ # Core v0.5 (backwards compatible)
80
+ "CostCalculator",
81
+ "Cost",
82
+ "ProviderType",
83
+ "PricingManager",
84
+ "DatabaseManager",
85
+ "CostDatabase",
86
+ # v0.6+ multi-provider cost models
87
+ "CostCalculatorV6",
88
+ "UsageData",
89
+ "CostModel",
90
+ "CostModelRegistry",
91
+ "ClaudeTokenModel",
92
+ "GPT4oTokenModel",
93
+ "GeminiCharacterModel",
94
+ "GroqSpeedTieredModel",
95
+ "DeepInfraTokenModel",
96
+ "TogetherAITokenModel",
97
+ # v0.7+ token counting (if available)
98
+ "TokenCounter",
99
+ "TokenCountResult",
100
+ "TokenCounterRegistry",
101
+ "TokenCounterCache",
102
+ # Budget enforcement (safety feature)
103
+ "BudgetEnforcer",
104
+ "BudgetLimit",
105
+ "BudgetStatus",
106
+ "BudgetPeriod",
107
+ "EnforcementAction",
108
+ "BudgetExceededError",
109
+ "get_global_enforcer",
110
+ "set_budget_limit",
111
+ ]
@@ -0,0 +1,265 @@
1
+ """
2
+ Hard budget enforcement and spending limits.
3
+
4
+ Prevents runaway costs by enforcing hard stops when budgets are exceeded.
5
+ This is a CRITICAL safety feature to prevent agent-driven cost explosions.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Optional, Callable, List
10
+ from enum import Enum
11
+ from datetime import datetime, timedelta
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class BudgetPeriod(Enum):
18
+ """Budget enforcement period."""
19
+ HOURLY = "hourly"
20
+ DAILY = "daily"
21
+ MONTHLY = "monthly"
22
+ YEARLY = "yearly"
23
+ TOTAL = "total" # Total lifetime budget
24
+
25
+
26
+ class EnforcementAction(Enum):
27
+ """Action to take when budget is exceeded."""
28
+ ALERT = "alert" # Log warning, continue
29
+ THROTTLE = "throttle" # Reduce rate, continue
30
+ STOP = "stop" # Hard stop, raise exception
31
+ WEBHOOK = "webhook" # Call external webhook
32
+
33
+
34
+ @dataclass
35
+ class BudgetLimit:
36
+ """Budget limit definition."""
37
+ limit_usd: float
38
+ period: BudgetPeriod
39
+ action: EnforcementAction
40
+ warning_threshold: float = 0.80 # Alert at 80% of budget
41
+ description: str = ""
42
+
43
+
44
+ @dataclass
45
+ class BudgetStatus:
46
+ """Current budget status."""
47
+ period: BudgetPeriod
48
+ limit_usd: float
49
+ spent_usd: float
50
+ remaining_usd: float
51
+ percent_used: float
52
+ is_exceeded: bool
53
+ is_warning: bool
54
+ timestamp: datetime
55
+
56
+
57
+ class BudgetEnforcer:
58
+ """
59
+ Enforces hard budget limits on API spending.
60
+
61
+ Prevents runaway costs from:
62
+ - Agentic loops (5-30x normal token consumption)
63
+ - Context window bloat (re-sending conversation history)
64
+ - Concurrent requests (multiple agents running simultaneously)
65
+ """
66
+
67
+ def __init__(self):
68
+ self.limits: List[BudgetLimit] = []
69
+ self.spent_tracking = {
70
+ BudgetPeriod.HOURLY: 0.0,
71
+ BudgetPeriod.DAILY: 0.0,
72
+ BudgetPeriod.MONTHLY: 0.0,
73
+ BudgetPeriod.YEARLY: 0.0,
74
+ BudgetPeriod.TOTAL: 0.0,
75
+ }
76
+ self.last_reset = {
77
+ BudgetPeriod.HOURLY: datetime.now(),
78
+ BudgetPeriod.DAILY: datetime.now(),
79
+ BudgetPeriod.MONTHLY: datetime.now(),
80
+ BudgetPeriod.YEARLY: datetime.now(),
81
+ }
82
+ self.webhook_callbacks: dict[EnforcementAction, List[Callable]] = {
83
+ action: [] for action in EnforcementAction
84
+ }
85
+
86
+ def add_limit(self, limit: BudgetLimit):
87
+ """Add a budget limit."""
88
+ self.limits.append(limit)
89
+ logger.info(f"Added budget limit: {limit.limit_usd} USD per {limit.period.value}")
90
+
91
+ def add_webhook(self, action: EnforcementAction, callback: Callable):
92
+ """Add callback for enforcement action."""
93
+ self.webhook_callbacks[action].append(callback)
94
+
95
+ def check_budget(self, cost_usd: float) -> BudgetStatus:
96
+ """
97
+ Check if a cost can be incurred within budget.
98
+
99
+ Raises BudgetExceededError if cost exceeds hard limit.
100
+ """
101
+ self._reset_periods()
102
+
103
+ # Check all applicable limits
104
+ exceeded_limits = []
105
+ for limit in self.limits:
106
+ new_spent = self.spent_tracking[limit.period] + cost_usd
107
+ is_exceeded = new_spent > limit.limit_usd
108
+ is_warning = new_spent > (limit.limit_usd * limit.warning_threshold)
109
+
110
+ if is_exceeded or is_warning:
111
+ status = BudgetStatus(
112
+ period=limit.period,
113
+ limit_usd=limit.limit_usd,
114
+ spent_usd=new_spent,
115
+ remaining_usd=max(0, limit.limit_usd - new_spent),
116
+ percent_used=(new_spent / limit.limit_usd) * 100,
117
+ is_exceeded=is_exceeded,
118
+ is_warning=is_warning and not is_exceeded,
119
+ timestamp=datetime.now()
120
+ )
121
+
122
+ if is_exceeded:
123
+ exceeded_limits.append((limit, status))
124
+ self._execute_action(limit.action, status)
125
+ elif is_warning:
126
+ logger.warning(
127
+ f"Budget warning: {status.percent_used:.1f}% of {limit.period.value} budget used "
128
+ f"({status.spent_usd:.2f}$ of {status.limit_usd}$)"
129
+ )
130
+
131
+ # If any limit is exceeded, determine what to do
132
+ if exceeded_limits:
133
+ limit, status = exceeded_limits[0]
134
+ if limit.action == EnforcementAction.STOP:
135
+ raise BudgetExceededError(
136
+ f"Budget exceeded for {limit.period.value}: "
137
+ f"{status.spent_usd:.2f}$ spent of {status.limit_usd}$ limit"
138
+ )
139
+
140
+ # Record the spend
141
+ for period in BudgetPeriod:
142
+ self.spent_tracking[period] += cost_usd
143
+
144
+ return status
145
+
146
+ def get_status(self) -> dict[BudgetPeriod, BudgetStatus]:
147
+ """Get current budget status for all periods."""
148
+ self._reset_periods()
149
+
150
+ status = {}
151
+ for limit in self.limits:
152
+ spent = self.spent_tracking[limit.period]
153
+ status[limit.period] = BudgetStatus(
154
+ period=limit.period,
155
+ limit_usd=limit.limit_usd,
156
+ spent_usd=spent,
157
+ remaining_usd=max(0, limit.limit_usd - spent),
158
+ percent_used=(spent / limit.limit_usd) * 100,
159
+ is_exceeded=spent > limit.limit_usd,
160
+ is_warning=spent > (limit.limit_usd * limit.warning_threshold),
161
+ timestamp=datetime.now()
162
+ )
163
+
164
+ return status
165
+
166
+ def reset_period(self, period: BudgetPeriod):
167
+ """Manually reset a budget period."""
168
+ self.spent_tracking[period] = 0.0
169
+ self.last_reset[period] = datetime.now()
170
+ logger.info(f"Reset {period.value} budget")
171
+
172
+ def _reset_periods(self):
173
+ """Reset periods that have elapsed."""
174
+ now = datetime.now()
175
+
176
+ # Hourly
177
+ if (now - self.last_reset[BudgetPeriod.HOURLY]).total_seconds() >= 3600:
178
+ self.spent_tracking[BudgetPeriod.HOURLY] = 0.0
179
+ self.last_reset[BudgetPeriod.HOURLY] = now
180
+
181
+ # Daily (reset at midnight)
182
+ if now.date() != self.last_reset[BudgetPeriod.DAILY].date():
183
+ self.spent_tracking[BudgetPeriod.DAILY] = 0.0
184
+ self.last_reset[BudgetPeriod.DAILY] = now
185
+
186
+ # Monthly (reset on 1st of month)
187
+ if now.month != self.last_reset[BudgetPeriod.MONTHLY].month:
188
+ self.spent_tracking[BudgetPeriod.MONTHLY] = 0.0
189
+ self.last_reset[BudgetPeriod.MONTHLY] = now
190
+
191
+ # Yearly (reset on Jan 1)
192
+ if now.year != self.last_reset[BudgetPeriod.YEARLY].year:
193
+ self.spent_tracking[BudgetPeriod.YEARLY] = 0.0
194
+ self.last_reset[BudgetPeriod.YEARLY] = now
195
+
196
+ def _execute_action(self, action: EnforcementAction, status: BudgetStatus):
197
+ """Execute the enforcement action."""
198
+ if action == EnforcementAction.ALERT:
199
+ logger.warning(f"Budget alert: {status.percent_used:.1f}% used ({status.spent_usd:.2f}$)")
200
+
201
+ elif action == EnforcementAction.THROTTLE:
202
+ logger.info(f"Throttling requests - budget at {status.percent_used:.1f}%")
203
+
204
+ elif action == EnforcementAction.STOP:
205
+ logger.error(
206
+ f"BUDGET EXCEEDED: {status.spent_usd:.2f}$ spent of {status.limit_usd}$ "
207
+ f"limit for {status.period.value}"
208
+ )
209
+
210
+ elif action == EnforcementAction.WEBHOOK:
211
+ for callback in self.webhook_callbacks[action]:
212
+ try:
213
+ callback(status)
214
+ except Exception as e:
215
+ logger.error(f"Webhook callback failed: {e}")
216
+
217
+ def suggest_budget_allocation(self, monthly_budget: float) -> dict:
218
+ """
219
+ Suggest budget allocation across periods based on typical usage patterns.
220
+
221
+ Helps teams avoid accidental overspend.
222
+ """
223
+ return {
224
+ "monthly_total": monthly_budget,
225
+ "daily_suggested": monthly_budget / 30,
226
+ "hourly_suggested": monthly_budget / (30 * 24),
227
+ "warning_threshold_pct": 80,
228
+ "hard_stop_action": "STOP",
229
+ "recommendation": (
230
+ "Set a daily limit at 3-5% of monthly budget (1/30) to catch runaway costs early. "
231
+ "Adjust if your usage is bursty. Set action=STOP for automated enforcement."
232
+ )
233
+ }
234
+
235
+
236
+ class BudgetExceededError(Exception):
237
+ """Raised when budget limit is exceeded with STOP action."""
238
+ pass
239
+
240
+
241
+ # Global budget enforcer instance (optional)
242
+ _global_enforcer: Optional[BudgetEnforcer] = None
243
+
244
+
245
+ def get_global_enforcer() -> BudgetEnforcer:
246
+ """Get or create global budget enforcer."""
247
+ global _global_enforcer
248
+ if _global_enforcer is None:
249
+ _global_enforcer = BudgetEnforcer()
250
+ return _global_enforcer
251
+
252
+
253
+ def set_budget_limit(
254
+ limit_usd: float,
255
+ period: BudgetPeriod = BudgetPeriod.DAILY,
256
+ action: EnforcementAction = EnforcementAction.STOP
257
+ ):
258
+ """Convenience function to set a budget limit."""
259
+ enforcer = get_global_enforcer()
260
+ limit = BudgetLimit(
261
+ limit_usd=limit_usd,
262
+ period=period,
263
+ action=action
264
+ )
265
+ enforcer.add_limit(limit)
pytokencalc/config.py ADDED
@@ -0,0 +1,45 @@
1
+ """Secure configuration management using environment variables."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ class Config:
8
+ """Application configuration from environment variables."""
9
+
10
+ # Server
11
+ SERVER_HOST = os.getenv("PYCOSTAUDIT_HOST", "localhost")
12
+ SERVER_PORT = int(os.getenv("PYCOSTAUDIT_PORT", "8000"))
13
+ DEBUG = os.getenv("PYCOSTAUDIT_DEBUG", "false").lower() == "true"
14
+
15
+ # Database
16
+ DATABASE_URL = os.getenv("PYTOKENCALC_DATABASE_URL", "sqlite:///~/.pytokencalc/costs.db")
17
+
18
+ # SMTP (Email)
19
+ SMTP_HOST = os.getenv("PYCOSTAUDIT_SMTP_HOST", "smtp.gmail.com")
20
+ SMTP_PORT = int(os.getenv("PYCOSTAUDIT_SMTP_PORT", "587"))
21
+ SMTP_USER = os.getenv("PYCOSTAUDIT_SMTP_USER")
22
+ SMTP_PASSWORD = os.getenv("PYCOSTAUDIT_SMTP_PASSWORD")
23
+ FROM_EMAIL = os.getenv("PYCOSTAUDIT_FROM_EMAIL", SMTP_USER)
24
+
25
+ # Slack
26
+ SLACK_WEBHOOK_URL = os.getenv("PYCOSTAUDIT_SLACK_WEBHOOK_URL")
27
+
28
+ # Twilio (SMS)
29
+ TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
30
+ TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
31
+ TWILIO_FROM_NUMBER = os.getenv("TWILIO_PHONE_NUMBER")
32
+
33
+ # Telemetry
34
+ JAEGER_HOST = os.getenv("JAEGER_HOST", "localhost")
35
+ JAEGER_PORT = int(os.getenv("JAEGER_PORT", "6831"))
36
+ PROMETHEUS_PORT = int(os.getenv("PROMETHEUS_PORT", "8000"))
37
+
38
+ @staticmethod
39
+ def validate():
40
+ """Validate required configuration."""
41
+ if not Config.SMTP_USER and not Config.SLACK_WEBHOOK_URL:
42
+ print("Warning: No SMTP or Slack credentials configured")
43
+ return True
44
+
45
+ config = Config()