token-budget-orchestrator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+ *.egg
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Node
15
+ node_modules/
16
+ .next/
17
+ out/
18
+ dist/
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
23
+ *.swp
24
+ *.swo
25
+
26
+ # OS
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Environment
31
+ .env
32
+ .env.local
33
+ .env.*.local
34
+
35
+ # Docker
36
+ docker-compose.override.yml
37
+
38
+ # Testing
39
+ .coverage
40
+ htmlcov/
41
+ .pytest_cache/
42
+ coverage/
43
+
44
+ # Build artifacts
45
+ *.whl
46
+ *.tar.gz
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: token-budget-orchestrator
3
+ Version: 0.1.0
4
+ Summary: Token Budget Orchestrator — Active budget governance for multi-agent LLM systems
5
+ Author: Ricardo Martins
6
+ License: MIT
7
+ Keywords: anthropic,budget,llm,multi-agent,openai,orchestration,tokens
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.25.0
18
+ Requires-Dist: pydantic>=2.0
19
+ Requires-Dist: tiktoken>=0.5.0
20
+ Provides-Extra: all
21
+ Requires-Dist: anthropic>=0.30.0; extra == 'all'
22
+ Requires-Dist: openai>=1.30.0; extra == 'all'
23
+ Provides-Extra: anthropic
24
+ Requires-Dist: anthropic>=0.30.0; extra == 'anthropic'
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.0; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
31
+ Provides-Extra: openai
32
+ Requires-Dist: openai>=1.30.0; extra == 'openai'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # TBO Python SDK
36
+
37
+ Active budget governance for multi-agent LLM systems.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install tbo[anthropic] # or tbo[openai] or tbo[all]
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from tbo import TBOClient, BudgetConfig
49
+
50
+ client = TBOClient(
51
+ provider="anthropic",
52
+ api_key="your-key",
53
+ workspace="my-project",
54
+ agent_id="support-bot",
55
+ budget=BudgetConfig(max_tokens=100_000, period="daily"),
56
+ )
57
+
58
+ response = client.messages.create(
59
+ model="claude-sonnet-4-20250514",
60
+ max_tokens=1024,
61
+ messages=[{"role": "user", "content": "Hello"}]
62
+ )
63
+ ```
@@ -0,0 +1,29 @@
1
+ # TBO Python SDK
2
+
3
+ Active budget governance for multi-agent LLM systems.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install tbo[anthropic] # or tbo[openai] or tbo[all]
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from tbo import TBOClient, BudgetConfig
15
+
16
+ client = TBOClient(
17
+ provider="anthropic",
18
+ api_key="your-key",
19
+ workspace="my-project",
20
+ agent_id="support-bot",
21
+ budget=BudgetConfig(max_tokens=100_000, period="daily"),
22
+ )
23
+
24
+ response = client.messages.create(
25
+ model="claude-sonnet-4-20250514",
26
+ max_tokens=1024,
27
+ messages=[{"role": "user", "content": "Hello"}]
28
+ )
29
+ ```
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "token-budget-orchestrator"
3
+ version = "0.1.0"
4
+ description = "Token Budget Orchestrator — Active budget governance for multi-agent LLM systems"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "Ricardo Martins" }
10
+ ]
11
+ keywords = ["llm", "tokens", "budget", "orchestration", "multi-agent", "anthropic", "openai"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+
23
+ dependencies = [
24
+ "httpx>=0.25.0",
25
+ "pydantic>=2.0",
26
+ "tiktoken>=0.5.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ anthropic = ["anthropic>=0.30.0"]
31
+ openai = ["openai>=1.30.0"]
32
+ all = ["anthropic>=0.30.0", "openai>=1.30.0"]
33
+ dev = [
34
+ "pytest>=7.0",
35
+ "pytest-asyncio>=0.21",
36
+ "pytest-cov>=4.0",
37
+ "ruff>=0.4.0",
38
+ "mypy>=1.0",
39
+ ]
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/tbo"]
47
+
48
+ [tool.ruff]
49
+ target-version = "py310"
50
+ line-length = 100
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "N", "W", "UP"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ asyncio_mode = "auto"
58
+
59
+ [tool.mypy]
60
+ python_version = "3.10"
61
+ strict = true
@@ -0,0 +1,22 @@
1
+ """Token Budget Orchestrator — Active budget governance for multi-agent LLM systems."""
2
+
3
+ from tbo.budget import BudgetCheckResult, BudgetConfig, BudgetExceededError, BudgetManager, OnExceed
4
+ from tbo.client import TBOClient
5
+ from tbo.models import AgentBudget, UsageRecord
6
+ from tbo.policy import Policy, PolicyAction, RoutingRule
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "TBOClient",
12
+ "BudgetManager",
13
+ "BudgetConfig",
14
+ "BudgetCheckResult",
15
+ "BudgetExceededError",
16
+ "OnExceed",
17
+ "Policy",
18
+ "PolicyAction",
19
+ "RoutingRule",
20
+ "UsageRecord",
21
+ "AgentBudget",
22
+ ]
@@ -0,0 +1,216 @@
1
+ """Budget management — tracks and enforces token/cost budgets per agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import threading
7
+ import time
8
+ from enum import Enum
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ from tbo.models import AgentBudget, BudgetPeriod, BudgetStatus
13
+
14
+ logger = logging.getLogger("tbo.budget")
15
+
16
+
17
+ class OnExceed(str, Enum):
18
+ BLOCK = "block"
19
+ FALLBACK = "fallback"
20
+ ALERT = "alert"
21
+
22
+
23
+ class BudgetConfig(BaseModel):
24
+ """Configuration for budget enforcement."""
25
+
26
+ max_tokens: int | None = None
27
+ max_cost_usd: float | None = None
28
+ period: BudgetPeriod = BudgetPeriod.DAILY
29
+ warning_threshold: float = Field(default=0.8, ge=0.0, le=1.0)
30
+ # Safety margin for output token estimation uncertainty
31
+ safety_margin: float = Field(default=0.15, ge=0.0, le=0.5)
32
+ # What to do when budget is exceeded
33
+ on_exceed: OnExceed = OnExceed.BLOCK
34
+ fallback_model: str | None = None
35
+
36
+
37
+ class BudgetCheckResult(BaseModel):
38
+ """Result of a budget check — tells caller what to do."""
39
+
40
+ allowed: bool = True
41
+ budget: AgentBudget
42
+ fallback_model: str | None = None
43
+ reason: str | None = None
44
+
45
+
46
+ class BudgetExceededError(Exception):
47
+ """Raised when a call would exceed the configured budget."""
48
+
49
+ def __init__(self, agent_id: str, budget: AgentBudget, requested_tokens: int):
50
+ self.agent_id = agent_id
51
+ self.budget = budget
52
+ self.requested_tokens = requested_tokens
53
+ remaining = (budget.max_tokens or 0) - budget.used_tokens
54
+ super().__init__(
55
+ f"Budget exceeded for agent '{agent_id}': "
56
+ f"requested ~{requested_tokens} tokens, "
57
+ f"remaining {remaining} tokens in {budget.period.value} period"
58
+ )
59
+
60
+
61
+ class BudgetManager:
62
+ """Manages token budgets for agents. Thread-safe, local-first.
63
+
64
+ In standalone mode (no TBO engine), budgets are tracked in-memory.
65
+ With TBO engine, syncs with Redis-backed central budget store.
66
+ """
67
+
68
+ def __init__(self):
69
+ self._budgets: dict[str, AgentBudget] = {}
70
+ self._configs: dict[str, BudgetConfig] = {}
71
+ self._lock = threading.Lock()
72
+ self._period_start: dict[str, float] = {}
73
+
74
+ def register_agent(self, workspace: str, agent_id: str, config: BudgetConfig) -> AgentBudget:
75
+ """Register an agent with a budget configuration."""
76
+ key = f"{workspace}:{agent_id}"
77
+ budget = AgentBudget(
78
+ agent_id=agent_id,
79
+ workspace=workspace,
80
+ max_tokens=config.max_tokens,
81
+ max_cost_usd=config.max_cost_usd,
82
+ period=config.period,
83
+ warning_threshold=config.warning_threshold,
84
+ )
85
+ with self._lock:
86
+ self._budgets[key] = budget
87
+ self._configs[key] = config
88
+ self._period_start[key] = time.time()
89
+ return budget
90
+
91
+ def check_budget(
92
+ self, workspace: str, agent_id: str, estimated_input_tokens: int
93
+ ) -> BudgetCheckResult:
94
+ """Check if agent has budget for a call. Resets if period expired.
95
+
96
+ Returns:
97
+ BudgetCheckResult with allowed=True/False and optional fallback_model.
98
+
99
+ Raises:
100
+ BudgetExceededError: Only if on_exceed="block" and budget is exceeded.
101
+ """
102
+ key = f"{workspace}:{agent_id}"
103
+ with self._lock:
104
+ budget = self._budgets.get(key)
105
+ if budget is None:
106
+ return BudgetCheckResult(
107
+ allowed=True,
108
+ budget=AgentBudget(agent_id=agent_id, workspace=workspace),
109
+ )
110
+
111
+ config = self._configs.get(key)
112
+
113
+ # Check if period has rolled over
114
+ self._maybe_reset_period(key, budget)
115
+
116
+ exceeded = False
117
+ reason = None
118
+
119
+ # Check token budget
120
+ if budget.max_tokens is not None:
121
+ remaining = budget.max_tokens - budget.used_tokens
122
+ if estimated_input_tokens > remaining:
123
+ exceeded = True
124
+ reason = (
125
+ f"Requested ~{estimated_input_tokens} tokens, "
126
+ f"remaining {remaining} in {budget.period.value} period"
127
+ )
128
+ else:
129
+ usage_ratio = budget.used_tokens / budget.max_tokens
130
+ if usage_ratio >= budget.warning_threshold:
131
+ budget.status = BudgetStatus.WARNING
132
+
133
+ # Check cost budget
134
+ if not exceeded and budget.max_cost_usd is not None:
135
+ usage_ratio = budget.used_cost_usd / budget.max_cost_usd
136
+ if usage_ratio >= 1.0:
137
+ exceeded = True
138
+ reason = (
139
+ f"Cost budget exhausted: ${budget.used_cost_usd:.2f} / "
140
+ f"${budget.max_cost_usd:.2f}"
141
+ )
142
+ elif usage_ratio >= budget.warning_threshold:
143
+ budget.status = BudgetStatus.WARNING
144
+
145
+ if not exceeded:
146
+ return BudgetCheckResult(allowed=True, budget=budget)
147
+
148
+ # Budget exceeded — decide action based on config
149
+ budget.status = BudgetStatus.EXCEEDED
150
+
151
+ if config and config.on_exceed == OnExceed.FALLBACK and config.fallback_model:
152
+ logger.info(
153
+ f"Budget exceeded for '{agent_id}', falling back to {config.fallback_model}"
154
+ )
155
+ return BudgetCheckResult(
156
+ allowed=True,
157
+ budget=budget,
158
+ fallback_model=config.fallback_model,
159
+ reason=reason,
160
+ )
161
+
162
+ if config and config.on_exceed == OnExceed.ALERT:
163
+ logger.warning(f"Budget exceeded for '{agent_id}': {reason}")
164
+ return BudgetCheckResult(
165
+ allowed=True,
166
+ budget=budget,
167
+ reason=reason,
168
+ )
169
+
170
+ # Default: block
171
+ raise BudgetExceededError(agent_id, budget, estimated_input_tokens)
172
+
173
+ def record_usage(
174
+ self, workspace: str, agent_id: str, tokens_used: int, cost_usd: float
175
+ ) -> AgentBudget:
176
+ """Record actual usage after a call completes."""
177
+ key = f"{workspace}:{agent_id}"
178
+ with self._lock:
179
+ budget = self._budgets.get(key)
180
+ if budget is None:
181
+ return AgentBudget(agent_id=agent_id, workspace=workspace)
182
+
183
+ budget.used_tokens += tokens_used
184
+ budget.used_cost_usd += cost_usd
185
+
186
+ # Update status
187
+ if budget.max_tokens and budget.used_tokens >= budget.max_tokens:
188
+ budget.status = BudgetStatus.EXCEEDED
189
+ elif budget.max_cost_usd and budget.used_cost_usd >= budget.max_cost_usd:
190
+ budget.status = BudgetStatus.EXCEEDED
191
+
192
+ return budget
193
+
194
+ def get_budget(self, workspace: str, agent_id: str) -> AgentBudget | None:
195
+ """Get current budget state for an agent."""
196
+ key = f"{workspace}:{agent_id}"
197
+ with self._lock:
198
+ return self._budgets.get(key)
199
+
200
+ def _maybe_reset_period(self, key: str, budget: AgentBudget) -> None:
201
+ """Reset counters if the budget period has rolled over."""
202
+ start = self._period_start.get(key, time.time())
203
+ elapsed = time.time() - start
204
+
205
+ period_seconds = {
206
+ BudgetPeriod.HOURLY: 3600,
207
+ BudgetPeriod.DAILY: 86400,
208
+ BudgetPeriod.WEEKLY: 604800,
209
+ BudgetPeriod.MONTHLY: 2592000,
210
+ }
211
+
212
+ if elapsed >= period_seconds[budget.period]:
213
+ budget.used_tokens = 0
214
+ budget.used_cost_usd = 0.0
215
+ budget.status = BudgetStatus.OK
216
+ self._period_start[key] = time.time()