steerplane 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.
- steerplane-0.1.0/PKG-INFO +63 -0
- steerplane-0.1.0/README.md +45 -0
- steerplane-0.1.0/pyproject.toml +26 -0
- steerplane-0.1.0/setup.cfg +4 -0
- steerplane-0.1.0/setup.py +41 -0
- steerplane-0.1.0/steerplane/__init__.py +69 -0
- steerplane-0.1.0/steerplane/client.py +129 -0
- steerplane-0.1.0/steerplane/config.py +49 -0
- steerplane-0.1.0/steerplane/cost_tracker.py +171 -0
- steerplane-0.1.0/steerplane/exceptions.py +68 -0
- steerplane-0.1.0/steerplane/guard.py +179 -0
- steerplane-0.1.0/steerplane/loop_detector.py +124 -0
- steerplane-0.1.0/steerplane/run_manager.py +266 -0
- steerplane-0.1.0/steerplane/telemetry.py +113 -0
- steerplane-0.1.0/steerplane/utils.py +57 -0
- steerplane-0.1.0/steerplane.egg-info/PKG-INFO +63 -0
- steerplane-0.1.0/steerplane.egg-info/SOURCES.txt +22 -0
- steerplane-0.1.0/steerplane.egg-info/dependency_links.txt +1 -0
- steerplane-0.1.0/steerplane.egg-info/requires.txt +5 -0
- steerplane-0.1.0/steerplane.egg-info/top_level.txt +2 -0
- steerplane-0.1.0/tests/__init__.py +1 -0
- steerplane-0.1.0/tests/test_cost_tracker.py +65 -0
- steerplane-0.1.0/tests/test_guard.py +60 -0
- steerplane-0.1.0/tests/test_loop_detector.py +100 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steerplane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Control Plane for Autonomous Systems
|
|
5
|
+
Home-page: https://github.com/vijaym2k6/SteerPlane
|
|
6
|
+
Author: SteerPlane
|
|
7
|
+
Author-email: SteerPlane <hello@steerplane.ai>
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: requests>=2.28.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: home-page
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
|
|
19
|
+
# SteerPlane SDK
|
|
20
|
+
|
|
21
|
+
**Agent Control Plane for Autonomous Systems**
|
|
22
|
+
|
|
23
|
+
> "Agents don't fail in the dark anymore."
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install steerplane
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Decorator API (Minimal Integration)
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from steerplane import guard
|
|
35
|
+
|
|
36
|
+
@guard(max_cost_usd=10, max_steps=50)
|
|
37
|
+
def run_agent():
|
|
38
|
+
agent.run()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Context Manager API (Full Control)
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from steerplane import SteerPlane
|
|
45
|
+
|
|
46
|
+
sp = SteerPlane(agent_id="my_bot")
|
|
47
|
+
|
|
48
|
+
with sp.run(max_cost_usd=10) as run:
|
|
49
|
+
run.log_step("query_db", tokens=380, cost=0.002)
|
|
50
|
+
run.log_step("generate_response", tokens=1240, cost=0.008)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- 🔄 **Loop Detection** — Automatically detects repeating agent behavior
|
|
56
|
+
- 💰 **Cost Limits** — Stop expensive agent runs before they blow budgets
|
|
57
|
+
- 🚫 **Step Limits** — Prevent runaway execution
|
|
58
|
+
- 📊 **Telemetry** — Full step-by-step execution tracking
|
|
59
|
+
- 🖥️ **Dashboard** — Visual execution monitoring
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
See [docs.steerplane.ai](https://docs.steerplane.ai) for full documentation.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SteerPlane SDK
|
|
2
|
+
|
|
3
|
+
**Agent Control Plane for Autonomous Systems**
|
|
4
|
+
|
|
5
|
+
> "Agents don't fail in the dark anymore."
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install steerplane
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Decorator API (Minimal Integration)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from steerplane import guard
|
|
17
|
+
|
|
18
|
+
@guard(max_cost_usd=10, max_steps=50)
|
|
19
|
+
def run_agent():
|
|
20
|
+
agent.run()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Context Manager API (Full Control)
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from steerplane import SteerPlane
|
|
27
|
+
|
|
28
|
+
sp = SteerPlane(agent_id="my_bot")
|
|
29
|
+
|
|
30
|
+
with sp.run(max_cost_usd=10) as run:
|
|
31
|
+
run.log_step("query_db", tokens=380, cost=0.002)
|
|
32
|
+
run.log_step("generate_response", tokens=1240, cost=0.008)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- 🔄 **Loop Detection** — Automatically detects repeating agent behavior
|
|
38
|
+
- 💰 **Cost Limits** — Stop expensive agent runs before they blow budgets
|
|
39
|
+
- 🚫 **Step Limits** — Prevent runaway execution
|
|
40
|
+
- 📊 **Telemetry** — Full step-by-step execution tracking
|
|
41
|
+
- 🖥️ **Dashboard** — Visual execution monitoring
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
See [docs.steerplane.ai](https://docs.steerplane.ai) for full documentation.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "steerplane"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agent Control Plane for Autonomous Systems"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "SteerPlane", email = "hello@steerplane.ai"}
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"requests>=2.28.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=7.0",
|
|
22
|
+
"pytest-cov",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="steerplane",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Agent Control Plane for Autonomous Systems — Runtime guards, loop detection, cost limits, and telemetry for AI agents.",
|
|
7
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="SteerPlane",
|
|
10
|
+
author_email="hello@steerplane.ai",
|
|
11
|
+
url="https://github.com/vijaym2k6/SteerPlane",
|
|
12
|
+
project_urls={
|
|
13
|
+
"Homepage": "https://steerplane.ai",
|
|
14
|
+
"Documentation": "https://docs.steerplane.ai",
|
|
15
|
+
"Repository": "https://github.com/vijaym2k6/SteerPlane",
|
|
16
|
+
"Issues": "https://github.com/vijaym2k6/SteerPlane/issues",
|
|
17
|
+
},
|
|
18
|
+
packages=find_packages(),
|
|
19
|
+
python_requires=">=3.10",
|
|
20
|
+
install_requires=[
|
|
21
|
+
"requests>=2.28.0",
|
|
22
|
+
],
|
|
23
|
+
extras_require={
|
|
24
|
+
"dev": [
|
|
25
|
+
"pytest>=7.0",
|
|
26
|
+
"pytest-cov",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
classifiers=[
|
|
30
|
+
"Development Status :: 3 - Alpha",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Topic :: Software Development :: Libraries",
|
|
38
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
39
|
+
],
|
|
40
|
+
keywords="ai agents guard safety monitoring telemetry llm",
|
|
41
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SteerPlane SDK
|
|
3
|
+
|
|
4
|
+
Agent Control Plane for Autonomous Systems.
|
|
5
|
+
"Agents don't fail in the dark anymore."
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from steerplane import guard, SteerPlane
|
|
9
|
+
|
|
10
|
+
# Decorator API (minimal integration)
|
|
11
|
+
@guard(max_cost_usd=10, max_steps=50)
|
|
12
|
+
def run_agent():
|
|
13
|
+
agent.run()
|
|
14
|
+
|
|
15
|
+
# Context Manager API (full control)
|
|
16
|
+
sp = SteerPlane(agent_id="my_bot")
|
|
17
|
+
with sp.run(max_cost_usd=10) as run:
|
|
18
|
+
run.log_step("search", tokens=100)
|
|
19
|
+
|
|
20
|
+
# Programmatic API
|
|
21
|
+
sp = SteerPlane(agent_id="my_bot")
|
|
22
|
+
run = sp.create_run(max_cost_usd=10)
|
|
23
|
+
run.start()
|
|
24
|
+
run.log_step("search", tokens=100)
|
|
25
|
+
run.end()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
from .guard import guard, SteerPlane, get_active_run
|
|
31
|
+
from .run_manager import RunManager
|
|
32
|
+
from .loop_detector import LoopDetector, detect_loop
|
|
33
|
+
from .cost_tracker import CostTracker
|
|
34
|
+
from .telemetry import TelemetryCollector, StepEvent
|
|
35
|
+
from .config import configure, get_config
|
|
36
|
+
from .exceptions import (
|
|
37
|
+
SteerPlaneError,
|
|
38
|
+
LoopDetectedError,
|
|
39
|
+
CostLimitExceeded,
|
|
40
|
+
StepLimitExceeded,
|
|
41
|
+
RunTerminatedError,
|
|
42
|
+
APIConnectionError,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
# Main APIs
|
|
47
|
+
"guard",
|
|
48
|
+
"SteerPlane",
|
|
49
|
+
"get_active_run",
|
|
50
|
+
# Core classes
|
|
51
|
+
"RunManager",
|
|
52
|
+
"LoopDetector",
|
|
53
|
+
"CostTracker",
|
|
54
|
+
"TelemetryCollector",
|
|
55
|
+
"StepEvent",
|
|
56
|
+
# Utilities
|
|
57
|
+
"detect_loop",
|
|
58
|
+
"configure",
|
|
59
|
+
"get_config",
|
|
60
|
+
# Exceptions
|
|
61
|
+
"SteerPlaneError",
|
|
62
|
+
"LoopDetectedError",
|
|
63
|
+
"CostLimitExceeded",
|
|
64
|
+
"StepLimitExceeded",
|
|
65
|
+
"RunTerminatedError",
|
|
66
|
+
"APIConnectionError",
|
|
67
|
+
# Metadata
|
|
68
|
+
"__version__",
|
|
69
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SteerPlane SDK — API Client
|
|
3
|
+
|
|
4
|
+
HTTP client that communicates with the SteerPlane API server.
|
|
5
|
+
Handles run lifecycle: start → log steps → end.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .config import get_config
|
|
13
|
+
from .exceptions import APIConnectionError
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("steerplane")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SteerPlaneClient:
|
|
19
|
+
"""
|
|
20
|
+
HTTP client for the SteerPlane API.
|
|
21
|
+
|
|
22
|
+
Sends run events, step telemetry, and receives commands.
|
|
23
|
+
Gracefully degrades if API is unavailable (SDK still works locally).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, api_url: str | None = None, api_key: str | None = None):
|
|
27
|
+
config = get_config()
|
|
28
|
+
self.api_url = (api_url or config.api_url).rstrip("/")
|
|
29
|
+
self.api_key = api_key or config.api_key
|
|
30
|
+
self.session = requests.Session()
|
|
31
|
+
self.session.headers.update({
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"User-Agent": "SteerPlane-SDK/0.1.0",
|
|
34
|
+
})
|
|
35
|
+
if self.api_key:
|
|
36
|
+
self.session.headers["Authorization"] = f"Bearer {self.api_key}"
|
|
37
|
+
|
|
38
|
+
self._api_available = True
|
|
39
|
+
|
|
40
|
+
def _request(self, method: str, path: str, **kwargs) -> dict | None:
|
|
41
|
+
"""Make an HTTP request to the API. Returns None if API unavailable."""
|
|
42
|
+
if not self._api_available:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
url = f"{self.api_url}{path}"
|
|
46
|
+
try:
|
|
47
|
+
response = self.session.request(method, url, timeout=5, **kwargs)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
return response.json()
|
|
50
|
+
except requests.ConnectionError:
|
|
51
|
+
self._api_available = False
|
|
52
|
+
logger.warning(
|
|
53
|
+
f"⚠️ SteerPlane API not reachable at {self.api_url}. "
|
|
54
|
+
f"Running in offline mode (guards still active, no dashboard data)."
|
|
55
|
+
)
|
|
56
|
+
return None
|
|
57
|
+
except requests.RequestException as e:
|
|
58
|
+
logger.warning(f"⚠️ API request failed: {e}")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def start_run(
|
|
62
|
+
self,
|
|
63
|
+
run_id: str,
|
|
64
|
+
agent_name: str,
|
|
65
|
+
max_cost_usd: float = 0,
|
|
66
|
+
max_steps: int = 0,
|
|
67
|
+
) -> dict | None:
|
|
68
|
+
"""Register a new run with the API."""
|
|
69
|
+
return self._request("POST", "/runs/start", json={
|
|
70
|
+
"run_id": run_id,
|
|
71
|
+
"agent_name": agent_name,
|
|
72
|
+
"max_cost_usd": max_cost_usd,
|
|
73
|
+
"max_steps": max_steps,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
def log_step(
|
|
77
|
+
self,
|
|
78
|
+
run_id: str,
|
|
79
|
+
step_number: int,
|
|
80
|
+
action: str,
|
|
81
|
+
tokens: int = 0,
|
|
82
|
+
cost_usd: float = 0.0,
|
|
83
|
+
latency_ms: float = 0.0,
|
|
84
|
+
status: str = "completed",
|
|
85
|
+
error: str | None = None,
|
|
86
|
+
metadata: dict | None = None,
|
|
87
|
+
) -> dict | None:
|
|
88
|
+
"""Log a step event to the API."""
|
|
89
|
+
return self._request("POST", "/runs/step", json={
|
|
90
|
+
"run_id": run_id,
|
|
91
|
+
"step_number": step_number,
|
|
92
|
+
"action": action,
|
|
93
|
+
"tokens": tokens,
|
|
94
|
+
"cost_usd": cost_usd,
|
|
95
|
+
"latency_ms": latency_ms,
|
|
96
|
+
"status": status,
|
|
97
|
+
"error": error,
|
|
98
|
+
"metadata": metadata or {},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
def end_run(
|
|
102
|
+
self,
|
|
103
|
+
run_id: str,
|
|
104
|
+
status: str = "completed",
|
|
105
|
+
total_cost: float = 0.0,
|
|
106
|
+
total_steps: int = 0,
|
|
107
|
+
error: str | None = None,
|
|
108
|
+
) -> dict | None:
|
|
109
|
+
"""Finalize a run."""
|
|
110
|
+
return self._request("POST", "/runs/end", json={
|
|
111
|
+
"run_id": run_id,
|
|
112
|
+
"status": status,
|
|
113
|
+
"total_cost": total_cost,
|
|
114
|
+
"total_steps": total_steps,
|
|
115
|
+
"error": error,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
def get_run(self, run_id: str) -> dict | None:
|
|
119
|
+
"""Fetch run details."""
|
|
120
|
+
return self._request("GET", f"/runs/{run_id}")
|
|
121
|
+
|
|
122
|
+
def list_runs(self, limit: int = 50, offset: int = 0) -> dict | None:
|
|
123
|
+
"""List recent runs."""
|
|
124
|
+
return self._request("GET", f"/runs?limit={limit}&offset={offset}")
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def is_connected(self) -> bool:
|
|
128
|
+
"""Check if API is reachable."""
|
|
129
|
+
return self._api_available
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SteerPlane SDK — Configuration
|
|
3
|
+
|
|
4
|
+
Default settings and environment variable overrides.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SteerPlaneConfig:
|
|
11
|
+
"""SDK configuration with environment variable overrides."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_url: str | None = None,
|
|
16
|
+
api_key: str | None = None,
|
|
17
|
+
agent_id: str | None = None,
|
|
18
|
+
default_max_cost_usd: float = 50.0,
|
|
19
|
+
default_max_steps: int = 200,
|
|
20
|
+
default_max_runtime_sec: int = 3600,
|
|
21
|
+
loop_window_size: int = 8,
|
|
22
|
+
enable_telemetry: bool = True,
|
|
23
|
+
log_to_console: bool = True,
|
|
24
|
+
):
|
|
25
|
+
self.api_url = api_url or os.getenv("STEERPLANE_API_URL", "http://localhost:8000")
|
|
26
|
+
self.api_key = api_key or os.getenv("STEERPLANE_API_KEY", "")
|
|
27
|
+
self.agent_id = agent_id or os.getenv("STEERPLANE_AGENT_ID", "default_agent")
|
|
28
|
+
self.default_max_cost_usd = default_max_cost_usd
|
|
29
|
+
self.default_max_steps = default_max_steps
|
|
30
|
+
self.default_max_runtime_sec = default_max_runtime_sec
|
|
31
|
+
self.loop_window_size = loop_window_size
|
|
32
|
+
self.enable_telemetry = enable_telemetry
|
|
33
|
+
self.log_to_console = log_to_console
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Global default config (can be overridden)
|
|
37
|
+
_config = SteerPlaneConfig()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_config() -> SteerPlaneConfig:
|
|
41
|
+
"""Get the current global configuration."""
|
|
42
|
+
return _config
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configure(**kwargs) -> SteerPlaneConfig:
|
|
46
|
+
"""Update global SDK configuration."""
|
|
47
|
+
global _config
|
|
48
|
+
_config = SteerPlaneConfig(**kwargs)
|
|
49
|
+
return _config
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SteerPlane SDK — Cost Tracker
|
|
3
|
+
|
|
4
|
+
Tracks cumulative token usage and cost per run.
|
|
5
|
+
Enforces cost limits and projects final cost.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from .exceptions import CostLimitExceeded
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Default pricing per token (can be overridden per model)
|
|
13
|
+
DEFAULT_PRICING = {
|
|
14
|
+
"gpt-4o": {"input": 0.0000025, "output": 0.000010},
|
|
15
|
+
"gpt-4o-mini": {"input": 0.00000015, "output": 0.0000006},
|
|
16
|
+
"gpt-4": {"input": 0.00003, "output": 0.00006},
|
|
17
|
+
"gpt-3.5-turbo": {"input": 0.0000005, "output": 0.0000015},
|
|
18
|
+
"claude-3-opus": {"input": 0.000015, "output": 0.000075},
|
|
19
|
+
"claude-3-sonnet": {"input": 0.000003, "output": 0.000015},
|
|
20
|
+
"claude-3-haiku": {"input": 0.00000025, "output": 0.00000125},
|
|
21
|
+
"gemini-pro": {"input": 0.00000025, "output": 0.0000005},
|
|
22
|
+
"default": {"input": 0.000002, "output": 0.000002},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class StepCost:
|
|
28
|
+
"""Cost breakdown for a single step."""
|
|
29
|
+
input_tokens: int = 0
|
|
30
|
+
output_tokens: int = 0
|
|
31
|
+
total_tokens: int = 0
|
|
32
|
+
cost_usd: float = 0.0
|
|
33
|
+
model: str = "default"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class CostSummary:
|
|
38
|
+
"""Cumulative cost summary for an entire run."""
|
|
39
|
+
total_input_tokens: int = 0
|
|
40
|
+
total_output_tokens: int = 0
|
|
41
|
+
total_tokens: int = 0
|
|
42
|
+
total_cost_usd: float = 0.0
|
|
43
|
+
step_costs: list[StepCost] = field(default_factory=list)
|
|
44
|
+
projected_final_cost: float = 0.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CostTracker:
|
|
48
|
+
"""
|
|
49
|
+
Tracks cumulative cost across an agent run.
|
|
50
|
+
|
|
51
|
+
Enforces cost limits and provides real-time cost projection.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, max_cost_usd: float = 50.0, model: str = "default"):
|
|
55
|
+
"""
|
|
56
|
+
Args:
|
|
57
|
+
max_cost_usd: Maximum allowed cost before termination.
|
|
58
|
+
model: Default model name for pricing lookup.
|
|
59
|
+
"""
|
|
60
|
+
self.max_cost_usd = max_cost_usd
|
|
61
|
+
self.model = model
|
|
62
|
+
self.total_cost: float = 0.0
|
|
63
|
+
self.total_tokens: int = 0
|
|
64
|
+
self.total_input_tokens: int = 0
|
|
65
|
+
self.total_output_tokens: int = 0
|
|
66
|
+
self.step_costs: list[StepCost] = []
|
|
67
|
+
|
|
68
|
+
def calculate_step_cost(
|
|
69
|
+
self,
|
|
70
|
+
input_tokens: int = 0,
|
|
71
|
+
output_tokens: int = 0,
|
|
72
|
+
total_tokens: int = 0,
|
|
73
|
+
model: str | None = None,
|
|
74
|
+
cost_override: float | None = None,
|
|
75
|
+
) -> StepCost:
|
|
76
|
+
"""
|
|
77
|
+
Calculate cost for a single step.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
input_tokens: Number of input/prompt tokens.
|
|
81
|
+
output_tokens: Number of output/completion tokens.
|
|
82
|
+
total_tokens: Total tokens (used if input/output not split).
|
|
83
|
+
model: Model name for pricing (overrides default).
|
|
84
|
+
cost_override: Directly specify cost (skips calculation).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
StepCost with the calculated cost.
|
|
88
|
+
"""
|
|
89
|
+
use_model = model or self.model
|
|
90
|
+
pricing = DEFAULT_PRICING.get(use_model, DEFAULT_PRICING["default"])
|
|
91
|
+
|
|
92
|
+
if cost_override is not None:
|
|
93
|
+
cost = cost_override
|
|
94
|
+
elif input_tokens > 0 or output_tokens > 0:
|
|
95
|
+
cost = (input_tokens * pricing["input"]) + (output_tokens * pricing["output"])
|
|
96
|
+
total_tokens = input_tokens + output_tokens
|
|
97
|
+
elif total_tokens > 0:
|
|
98
|
+
# If no split, assume 50/50 input/output
|
|
99
|
+
avg_price = (pricing["input"] + pricing["output"]) / 2
|
|
100
|
+
cost = total_tokens * avg_price
|
|
101
|
+
else:
|
|
102
|
+
cost = 0.0
|
|
103
|
+
|
|
104
|
+
step_cost = StepCost(
|
|
105
|
+
input_tokens=input_tokens,
|
|
106
|
+
output_tokens=output_tokens,
|
|
107
|
+
total_tokens=total_tokens,
|
|
108
|
+
cost_usd=cost,
|
|
109
|
+
model=use_model,
|
|
110
|
+
)
|
|
111
|
+
return step_cost
|
|
112
|
+
|
|
113
|
+
def add_step(self, step_cost: StepCost) -> float:
|
|
114
|
+
"""
|
|
115
|
+
Add a step's cost to the running total and enforce limits.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
step_cost: The StepCost to add.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The new cumulative cost.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
CostLimitExceeded: If cost exceeds the configured limit.
|
|
125
|
+
"""
|
|
126
|
+
self.total_cost += step_cost.cost_usd
|
|
127
|
+
self.total_tokens += step_cost.total_tokens
|
|
128
|
+
self.total_input_tokens += step_cost.input_tokens
|
|
129
|
+
self.total_output_tokens += step_cost.output_tokens
|
|
130
|
+
self.step_costs.append(step_cost)
|
|
131
|
+
|
|
132
|
+
if self.total_cost > self.max_cost_usd:
|
|
133
|
+
raise CostLimitExceeded(self.total_cost, self.max_cost_usd)
|
|
134
|
+
|
|
135
|
+
return self.total_cost
|
|
136
|
+
|
|
137
|
+
def project_final_cost(
|
|
138
|
+
self, steps_completed: int, expected_total_steps: int
|
|
139
|
+
) -> float:
|
|
140
|
+
"""
|
|
141
|
+
Project the final cost based on current spending rate.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
steps_completed: Number of steps completed so far.
|
|
145
|
+
expected_total_steps: Expected total number of steps.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Projected final cost in USD.
|
|
149
|
+
"""
|
|
150
|
+
if steps_completed <= 0:
|
|
151
|
+
return 0.0
|
|
152
|
+
projected = self.total_cost * (expected_total_steps / steps_completed)
|
|
153
|
+
return projected
|
|
154
|
+
|
|
155
|
+
def get_summary(self) -> CostSummary:
|
|
156
|
+
"""Get the current cost summary."""
|
|
157
|
+
return CostSummary(
|
|
158
|
+
total_input_tokens=self.total_input_tokens,
|
|
159
|
+
total_output_tokens=self.total_output_tokens,
|
|
160
|
+
total_tokens=self.total_tokens,
|
|
161
|
+
total_cost_usd=self.total_cost,
|
|
162
|
+
step_costs=list(self.step_costs),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def reset(self):
|
|
166
|
+
"""Reset all tracking."""
|
|
167
|
+
self.total_cost = 0.0
|
|
168
|
+
self.total_tokens = 0
|
|
169
|
+
self.total_input_tokens = 0
|
|
170
|
+
self.total_output_tokens = 0
|
|
171
|
+
self.step_costs.clear()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SteerPlane SDK — Custom Exceptions
|
|
3
|
+
|
|
4
|
+
All errors raised by the SteerPlane guard system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SteerPlaneError(Exception):
|
|
9
|
+
"""Base exception for all SteerPlane errors."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LoopDetectedError(SteerPlaneError):
|
|
14
|
+
"""Raised when a repeating action pattern is detected."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, pattern: list, window_size: int):
|
|
17
|
+
self.pattern = pattern
|
|
18
|
+
self.window_size = window_size
|
|
19
|
+
super().__init__(
|
|
20
|
+
f"🔄 Loop detected! Repeating pattern {pattern} "
|
|
21
|
+
f"found in last {window_size} actions. Run terminated."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CostLimitExceeded(SteerPlaneError):
|
|
26
|
+
"""Raised when cumulative run cost exceeds the configured limit."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, current_cost: float, max_cost: float):
|
|
29
|
+
self.current_cost = current_cost
|
|
30
|
+
self.max_cost = max_cost
|
|
31
|
+
super().__init__(
|
|
32
|
+
f"💰 Cost limit exceeded! Current: ${current_cost:.4f}, "
|
|
33
|
+
f"Limit: ${max_cost:.2f}. Run terminated."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StepLimitExceeded(SteerPlaneError):
|
|
38
|
+
"""Raised when the number of steps exceeds the configured limit."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, current_steps: int, max_steps: int):
|
|
41
|
+
self.current_steps = current_steps
|
|
42
|
+
self.max_steps = max_steps
|
|
43
|
+
super().__init__(
|
|
44
|
+
f"🚫 Step limit exceeded! Steps: {current_steps}, "
|
|
45
|
+
f"Limit: {max_steps}. Run terminated."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RunTerminatedError(SteerPlaneError):
|
|
50
|
+
"""Raised when a run is forcefully terminated."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, run_id: str, reason: str = "Manual termination"):
|
|
53
|
+
self.run_id = run_id
|
|
54
|
+
self.reason = reason
|
|
55
|
+
super().__init__(
|
|
56
|
+
f"⛔ Run {run_id} terminated. Reason: {reason}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class APIConnectionError(SteerPlaneError):
|
|
61
|
+
"""Raised when the SDK cannot connect to the SteerPlane API."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, url: str, detail: str = ""):
|
|
64
|
+
self.url = url
|
|
65
|
+
self.detail = detail
|
|
66
|
+
super().__init__(
|
|
67
|
+
f"🔌 Cannot connect to SteerPlane API at {url}. {detail}"
|
|
68
|
+
)
|