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.
- token_budget_orchestrator-0.1.0/.gitignore +46 -0
- token_budget_orchestrator-0.1.0/PKG-INFO +63 -0
- token_budget_orchestrator-0.1.0/README.md +29 -0
- token_budget_orchestrator-0.1.0/pyproject.toml +61 -0
- token_budget_orchestrator-0.1.0/src/tbo/__init__.py +22 -0
- token_budget_orchestrator-0.1.0/src/tbo/budget.py +216 -0
- token_budget_orchestrator-0.1.0/src/tbo/client.py +281 -0
- token_budget_orchestrator-0.1.0/src/tbo/models.py +115 -0
- token_budget_orchestrator-0.1.0/src/tbo/policy.py +124 -0
- token_budget_orchestrator-0.1.0/src/tbo/telemetry.py +123 -0
- token_budget_orchestrator-0.1.0/src/tbo/tokenizer.py +83 -0
- token_budget_orchestrator-0.1.0/tests/test_budget.py +128 -0
- token_budget_orchestrator-0.1.0/tests/test_client.py +128 -0
- token_budget_orchestrator-0.1.0/tests/test_integration.py +237 -0
- token_budget_orchestrator-0.1.0/tests/test_policy.py +162 -0
|
@@ -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()
|