cost-intel 0.1.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.
- cost_intel/__init__.py +3 -0
- cost_intel/__main__.py +6 -0
- cost_intel/adapters/__init__.py +1 -0
- cost_intel/adapters/braintrust.py +67 -0
- cost_intel/adapters/eval_harness.py +53 -0
- cost_intel/alerts.py +134 -0
- cost_intel/budget.py +74 -0
- cost_intel/cli.py +950 -0
- cost_intel/compare.py +72 -0
- cost_intel/config.py +47 -0
- cost_intel/db.py +65 -0
- cost_intel/duration.py +43 -0
- cost_intel/estimate.py +58 -0
- cost_intel/gate.py +81 -0
- cost_intel/guard.py +43 -0
- cost_intel/ingest.py +332 -0
- cost_intel/migration_runner.py +100 -0
- cost_intel/migrations/001_initial.sql +68 -0
- cost_intel/migrations/002_add_quality.sql +58 -0
- cost_intel/migrations/003_add_trace_ids.sql +10 -0
- cost_intel/optimize.py +126 -0
- cost_intel/otel.py +154 -0
- cost_intel/pricing.py +222 -0
- cost_intel/prompt_opt.py +107 -0
- cost_intel/quality.py +205 -0
- cost_intel/record.py +171 -0
- cost_intel/report.py +137 -0
- cost_intel/trends.py +62 -0
- cost_intel/utils.py +40 -0
- cost_intel-0.1.0.dist-info/METADATA +133 -0
- cost_intel-0.1.0.dist-info/RECORD +33 -0
- cost_intel-0.1.0.dist-info/WHEEL +4 -0
- cost_intel-0.1.0.dist-info/entry_points.txt +2 -0
cost_intel/__init__.py
ADDED
cost_intel/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Quality score import adapters."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Braintrust adapter — import quality scores via the Braintrust REST API."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from cost_intel.quality import import_score
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def import_from_api(
|
|
11
|
+
api_key: str,
|
|
12
|
+
project_id: str,
|
|
13
|
+
experiment_id: Optional[str] = None,
|
|
14
|
+
source: str = "braintrust",
|
|
15
|
+
base_url: str = "https://api.braintrust.dev/v1",
|
|
16
|
+
) -> int:
|
|
17
|
+
"""Fetch quality scores from Braintrust and import them.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
api_key: Braintrust API key (sent as ``Authorization: Bearer``).
|
|
21
|
+
project_id: Braintrust project identifier.
|
|
22
|
+
experiment_id: Optional experiment id. If omitted, all
|
|
23
|
+
experiments under the project are imported.
|
|
24
|
+
source: Provenance label written to ``quality_scores.source``.
|
|
25
|
+
base_url: Braintrust API base URL.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Count of imported rows.
|
|
29
|
+
"""
|
|
30
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
31
|
+
count = 0
|
|
32
|
+
|
|
33
|
+
with httpx.Client(base_url=base_url, headers=headers, timeout=30) as client:
|
|
34
|
+
if experiment_id:
|
|
35
|
+
exp_ids = [experiment_id]
|
|
36
|
+
else:
|
|
37
|
+
resp = client.get(f"/projects/{project_id}/experiments")
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
experiments = resp.json().get("data", [])
|
|
40
|
+
exp_ids = [exp["id"] for exp in experiments]
|
|
41
|
+
|
|
42
|
+
for eid in exp_ids:
|
|
43
|
+
resp = client.get(f"/experiments/{eid}/events")
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
events = resp.json().get("data", [])
|
|
46
|
+
|
|
47
|
+
for event in events:
|
|
48
|
+
run_id = event.get("run_id") or event.get("id")
|
|
49
|
+
scores = event.get("scores", {})
|
|
50
|
+
if not run_id or not scores:
|
|
51
|
+
continue
|
|
52
|
+
score_val = scores.get("quality") or scores.get("score")
|
|
53
|
+
if score_val is None:
|
|
54
|
+
numeric = [
|
|
55
|
+
v for v in scores.values() if isinstance(v, (int, float))
|
|
56
|
+
]
|
|
57
|
+
score_val = numeric[0] if numeric else None
|
|
58
|
+
if score_val is not None:
|
|
59
|
+
import_score(
|
|
60
|
+
run_id=str(run_id),
|
|
61
|
+
score=float(score_val),
|
|
62
|
+
source=source,
|
|
63
|
+
eval_dimensions=scores if len(scores) > 1 else None,
|
|
64
|
+
)
|
|
65
|
+
count += 1
|
|
66
|
+
|
|
67
|
+
return count
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Eval Harness adapter — import quality scores from a SQLite database."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
|
|
5
|
+
from cost_intel.quality import import_score
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def import_from_db(
|
|
9
|
+
db_path: str,
|
|
10
|
+
source: str = "eval_harness",
|
|
11
|
+
run_id_column: str = "run_id",
|
|
12
|
+
score_column: str = "score",
|
|
13
|
+
) -> int:
|
|
14
|
+
"""Read scores from an Eval Harness SQLite DB and import them.
|
|
15
|
+
|
|
16
|
+
The adapter tries the ``results`` table first, then ``eval_results``
|
|
17
|
+
as a fallback. Returns ``0`` if neither table exists.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
db_path: Filesystem path to the Eval Harness database.
|
|
21
|
+
source: Provenance label written to ``quality_scores.source``.
|
|
22
|
+
run_id_column: Name of the column holding the run id.
|
|
23
|
+
score_column: Name of the column holding the score.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Count of imported rows.
|
|
27
|
+
"""
|
|
28
|
+
conn = sqlite3.connect(db_path)
|
|
29
|
+
conn.row_factory = sqlite3.Row
|
|
30
|
+
rows: list = []
|
|
31
|
+
try:
|
|
32
|
+
try:
|
|
33
|
+
rows = conn.execute(
|
|
34
|
+
f"SELECT {run_id_column}, {score_column} FROM results"
|
|
35
|
+
).fetchall()
|
|
36
|
+
except sqlite3.OperationalError:
|
|
37
|
+
try:
|
|
38
|
+
rows = conn.execute(
|
|
39
|
+
f"SELECT {run_id_column}, {score_column} FROM eval_results"
|
|
40
|
+
).fetchall()
|
|
41
|
+
except sqlite3.OperationalError:
|
|
42
|
+
return 0
|
|
43
|
+
finally:
|
|
44
|
+
conn.close()
|
|
45
|
+
|
|
46
|
+
count = 0
|
|
47
|
+
for row in rows:
|
|
48
|
+
run_id = str(row[run_id_column]) if row[run_id_column] else None
|
|
49
|
+
score = float(row[score_column]) if row[score_column] is not None else None
|
|
50
|
+
if run_id and score is not None:
|
|
51
|
+
import_score(run_id=run_id, score=score, source=source)
|
|
52
|
+
count += 1
|
|
53
|
+
return count
|
cost_intel/alerts.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Budget alert dispatch — Slack webhook + SMTP email."""
|
|
2
|
+
|
|
3
|
+
import smtplib
|
|
4
|
+
from email.mime.text import MIMEText
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from cost_intel.budget import get_budget_status
|
|
10
|
+
from cost_intel.config import load_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def send_slack_alert(webhook_url: str, message: str) -> bool:
|
|
14
|
+
"""Send alert to Slack via incoming webhook.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
webhook_url: Slack incoming webhook URL.
|
|
18
|
+
message: Plain text body for the Slack message.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
True on HTTP 200 response, False otherwise (including empty URL
|
|
22
|
+
or any exception).
|
|
23
|
+
"""
|
|
24
|
+
if not webhook_url:
|
|
25
|
+
return False
|
|
26
|
+
try:
|
|
27
|
+
resp = httpx.post(
|
|
28
|
+
webhook_url,
|
|
29
|
+
json={"text": message},
|
|
30
|
+
timeout=10.0,
|
|
31
|
+
)
|
|
32
|
+
return resp.status_code == 200
|
|
33
|
+
except Exception:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def send_email_alert(
|
|
38
|
+
smtp_host: str,
|
|
39
|
+
smtp_from: str,
|
|
40
|
+
recipients: list[str],
|
|
41
|
+
subject: str,
|
|
42
|
+
body: str,
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""Send alert via SMTP.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
smtp_host: SMTP server hostname.
|
|
48
|
+
smtp_from: Sender address.
|
|
49
|
+
recipients: List of recipient addresses.
|
|
50
|
+
subject: Email subject.
|
|
51
|
+
body: Plain text email body.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True on success, False if smtp_host or recipients are empty,
|
|
55
|
+
or on any exception.
|
|
56
|
+
"""
|
|
57
|
+
if not smtp_host or not recipients:
|
|
58
|
+
return False
|
|
59
|
+
try:
|
|
60
|
+
msg = MIMEText(body)
|
|
61
|
+
msg["Subject"] = subject
|
|
62
|
+
msg["From"] = smtp_from
|
|
63
|
+
msg["To"] = ", ".join(recipients)
|
|
64
|
+
|
|
65
|
+
with smtplib.SMTP(smtp_host) as server:
|
|
66
|
+
server.sendmail(smtp_from, recipients, msg.as_string())
|
|
67
|
+
return True
|
|
68
|
+
except Exception:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _build_alert_message(status: dict[str, Any]) -> str:
|
|
73
|
+
"""Build a human-readable alert message from a budget status dict."""
|
|
74
|
+
return (
|
|
75
|
+
"⚠️ Cost Intelligence Budget Alert\n"
|
|
76
|
+
f"Budget: ${status['monthly']:.2f}/month\n"
|
|
77
|
+
f"Spent: ${status['spent']:.2f} ({status['percent_used']:.1f}%)\n"
|
|
78
|
+
f"Remaining: ${status['remaining']:.2f}\n"
|
|
79
|
+
f"Alert threshold: {status['alert_threshold']}%"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def check_and_alert() -> dict[str, Any]:
|
|
84
|
+
"""Check budget status and dispatch alerts when threshold is reached.
|
|
85
|
+
|
|
86
|
+
Reads ``slack_webhook_url``, ``smtp_host``, ``smtp_from``, and
|
|
87
|
+
``alert_recipients`` from config and routes the alert message to each
|
|
88
|
+
configured channel.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict with keys ``triggered`` (bool), ``alert_sent`` (bool), and
|
|
92
|
+
``message`` (str). ``alert_sent`` is True when at least one
|
|
93
|
+
channel reported a successful dispatch.
|
|
94
|
+
"""
|
|
95
|
+
result: dict[str, Any] = {
|
|
96
|
+
"triggered": False,
|
|
97
|
+
"alert_sent": False,
|
|
98
|
+
"message": "",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
status = get_budget_status()
|
|
102
|
+
if not status["budget_set"]:
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
if status["percent_used"] < status["alert_threshold"]:
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
message = _build_alert_message(status)
|
|
109
|
+
result["triggered"] = True
|
|
110
|
+
result["message"] = message
|
|
111
|
+
|
|
112
|
+
cfg = load_config()
|
|
113
|
+
slack_webhook_url = cfg.get("slack_webhook_url", "") or ""
|
|
114
|
+
smtp_host = cfg.get("smtp_host", "") or ""
|
|
115
|
+
smtp_from = cfg.get("smtp_from", "") or ""
|
|
116
|
+
recipients = cfg.get("alert_recipients", []) or []
|
|
117
|
+
|
|
118
|
+
any_sent = False
|
|
119
|
+
if slack_webhook_url:
|
|
120
|
+
any_sent = send_slack_alert(slack_webhook_url, message) or any_sent
|
|
121
|
+
if smtp_host and recipients:
|
|
122
|
+
any_sent = (
|
|
123
|
+
send_email_alert(
|
|
124
|
+
smtp_host=smtp_host,
|
|
125
|
+
smtp_from=smtp_from,
|
|
126
|
+
recipients=list(recipients),
|
|
127
|
+
subject="Cost Intelligence Budget Alert",
|
|
128
|
+
body=message,
|
|
129
|
+
)
|
|
130
|
+
or any_sent
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
result["alert_sent"] = any_sent
|
|
134
|
+
return result
|
cost_intel/budget.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Budget management — set, status, and alert tracking."""
|
|
2
|
+
|
|
3
|
+
from cost_intel.db import connect
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def set_budget(monthly: float, alert_threshold: int = 80) -> None:
|
|
7
|
+
"""Set the monthly budget and alert threshold.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
monthly: Monthly budget in USD.
|
|
11
|
+
alert_threshold: Percentage at which to trigger alerts (0-100).
|
|
12
|
+
"""
|
|
13
|
+
with connect() as conn:
|
|
14
|
+
conn.execute(
|
|
15
|
+
"INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)",
|
|
16
|
+
("monthly_budget", str(monthly)),
|
|
17
|
+
)
|
|
18
|
+
conn.execute(
|
|
19
|
+
"INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)",
|
|
20
|
+
("alert_threshold", str(alert_threshold)),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_budget_status() -> dict:
|
|
25
|
+
"""Get current budget status including spending.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dict with budget_set, monthly, alert_threshold, spent,
|
|
29
|
+
remaining, percent_used.
|
|
30
|
+
"""
|
|
31
|
+
with connect() as conn:
|
|
32
|
+
budget_row = conn.execute(
|
|
33
|
+
"SELECT value FROM config WHERE key = ?",
|
|
34
|
+
("monthly_budget",),
|
|
35
|
+
).fetchone()
|
|
36
|
+
threshold_row = conn.execute(
|
|
37
|
+
"SELECT value FROM config WHERE key = ?",
|
|
38
|
+
("alert_threshold",),
|
|
39
|
+
).fetchone()
|
|
40
|
+
|
|
41
|
+
if budget_row is None:
|
|
42
|
+
return {
|
|
43
|
+
"budget_set": False,
|
|
44
|
+
"monthly": None,
|
|
45
|
+
"alert_threshold": None,
|
|
46
|
+
"spent": 0.0,
|
|
47
|
+
"remaining": None,
|
|
48
|
+
"percent_used": 0.0,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
monthly = float(budget_row["value"])
|
|
52
|
+
alert_threshold = int(threshold_row["value"]) if threshold_row else 80
|
|
53
|
+
|
|
54
|
+
# Calculate current month spending
|
|
55
|
+
with connect() as conn:
|
|
56
|
+
spent_row = conn.execute(
|
|
57
|
+
"SELECT COALESCE(SUM(call_cost), 0) as spent "
|
|
58
|
+
"FROM cost_run_calls crc "
|
|
59
|
+
"JOIN cost_runs cr ON crc.run_id = cr.run_id "
|
|
60
|
+
"WHERE cr.started_at >= date('now', 'start of month')"
|
|
61
|
+
).fetchone()
|
|
62
|
+
spent = float(spent_row["spent"]) if spent_row else 0.0
|
|
63
|
+
|
|
64
|
+
remaining = max(0.0, monthly - spent)
|
|
65
|
+
percent_used = (spent / monthly * 100) if monthly > 0 else 0.0
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"budget_set": True,
|
|
69
|
+
"monthly": monthly,
|
|
70
|
+
"alert_threshold": alert_threshold,
|
|
71
|
+
"spent": round(spent, 2),
|
|
72
|
+
"remaining": round(remaining, 2),
|
|
73
|
+
"percent_used": round(percent_used, 1),
|
|
74
|
+
}
|