buildshield-mcp 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.
- buildshield_mcp-0.1.0/.gitignore +37 -0
- buildshield_mcp-0.1.0/PKG-INFO +65 -0
- buildshield_mcp-0.1.0/README.md +52 -0
- buildshield_mcp-0.1.0/buildshield_mcp/__init__.py +3 -0
- buildshield_mcp-0.1.0/buildshield_mcp/server.py +232 -0
- buildshield_mcp-0.1.0/buildshield_mcp/tools.py +546 -0
- buildshield_mcp-0.1.0/pyproject.toml +23 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
backend/venv/
|
|
5
|
+
backend/.env
|
|
6
|
+
backend/.env.local
|
|
7
|
+
|
|
8
|
+
# Node
|
|
9
|
+
frontend/node_modules/
|
|
10
|
+
frontend/.next/
|
|
11
|
+
frontend/.env.local
|
|
12
|
+
frontend/.env
|
|
13
|
+
|
|
14
|
+
# Env files (catch-all)
|
|
15
|
+
*.env
|
|
16
|
+
frontend/.playwright-cli/
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
|
|
22
|
+
# OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
25
|
+
|
|
26
|
+
# Worktrees
|
|
27
|
+
.worktrees/
|
|
28
|
+
|
|
29
|
+
# Claude-Flow dev tool state (local only)
|
|
30
|
+
.claude-flow/
|
|
31
|
+
.swarm/
|
|
32
|
+
|
|
33
|
+
# Local/test artifacts
|
|
34
|
+
*.json.bak
|
|
35
|
+
intense_chat_export.json
|
|
36
|
+
models_output.json
|
|
37
|
+
test_output.json
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buildshield-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for BuildShield — AI cost intelligence for agents
|
|
5
|
+
Project-URL: Homepage, https://thebuildshield.com
|
|
6
|
+
Project-URL: Repository, https://github.com/Dev-31/theBuildShield.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: ai,buildshield,cost,llm,mcp
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: mcp>=1.0.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# buildshield-mcp
|
|
15
|
+
|
|
16
|
+
MCP server for BuildShield — AI cost intelligence for agents.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install buildshield-mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configure (Claude Desktop / Cursor)
|
|
25
|
+
|
|
26
|
+
Add to your MCP config (`~/.config/claude/claude_desktop_config.json`):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"buildshield": {
|
|
32
|
+
"command": "buildshield-mcp",
|
|
33
|
+
"env": {
|
|
34
|
+
"BUILDSHIELD_API_KEY": "bsk_..."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Get your API key at [thebuildshield.com/settings](https://thebuildshield.com/settings).
|
|
42
|
+
|
|
43
|
+
## Tools
|
|
44
|
+
|
|
45
|
+
| Tool | Description | Requires key? |
|
|
46
|
+
|------|-------------|---------------|
|
|
47
|
+
| `buildshield_check_cost` | Exact cost for model + token count | No — offline |
|
|
48
|
+
| `buildshield_recommend_model` | Best models for a task with filters | No — offline |
|
|
49
|
+
| `buildshield_get_pricing` | Browse all model pricing | No — offline |
|
|
50
|
+
| `buildshield_optimize_prompt` | Rule-based prompt compression (20-40% savings) | No — offline |
|
|
51
|
+
| `buildshield_check_budget` | Check remaining budget for your API key | Yes |
|
|
52
|
+
| `buildshield_log_usage` | Log usage to your dashboard | Optional |
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
"What does claude-sonnet-4.6 cost for 50K input + 10K output tokens?"
|
|
58
|
+
→ buildshield_check_cost(model="claude-sonnet-4.6", input_tokens=50000, output_tokens=10000)
|
|
59
|
+
|
|
60
|
+
"What's the cheapest model with at least 200K context?"
|
|
61
|
+
→ buildshield_recommend_model(task="long document analysis", min_context_tokens=200000)
|
|
62
|
+
|
|
63
|
+
"Compress this system prompt"
|
|
64
|
+
→ buildshield_optimize_prompt(text="...", aggressiveness="medium")
|
|
65
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# buildshield-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for BuildShield — AI cost intelligence for agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install buildshield-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure (Claude Desktop / Cursor)
|
|
12
|
+
|
|
13
|
+
Add to your MCP config (`~/.config/claude/claude_desktop_config.json`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"buildshield": {
|
|
19
|
+
"command": "buildshield-mcp",
|
|
20
|
+
"env": {
|
|
21
|
+
"BUILDSHIELD_API_KEY": "bsk_..."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Get your API key at [thebuildshield.com/settings](https://thebuildshield.com/settings).
|
|
29
|
+
|
|
30
|
+
## Tools
|
|
31
|
+
|
|
32
|
+
| Tool | Description | Requires key? |
|
|
33
|
+
|------|-------------|---------------|
|
|
34
|
+
| `buildshield_check_cost` | Exact cost for model + token count | No — offline |
|
|
35
|
+
| `buildshield_recommend_model` | Best models for a task with filters | No — offline |
|
|
36
|
+
| `buildshield_get_pricing` | Browse all model pricing | No — offline |
|
|
37
|
+
| `buildshield_optimize_prompt` | Rule-based prompt compression (20-40% savings) | No — offline |
|
|
38
|
+
| `buildshield_check_budget` | Check remaining budget for your API key | Yes |
|
|
39
|
+
| `buildshield_log_usage` | Log usage to your dashboard | Optional |
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
"What does claude-sonnet-4.6 cost for 50K input + 10K output tokens?"
|
|
45
|
+
→ buildshield_check_cost(model="claude-sonnet-4.6", input_tokens=50000, output_tokens=10000)
|
|
46
|
+
|
|
47
|
+
"What's the cheapest model with at least 200K context?"
|
|
48
|
+
→ buildshield_recommend_model(task="long document analysis", min_context_tokens=200000)
|
|
49
|
+
|
|
50
|
+
"Compress this system prompt"
|
|
51
|
+
→ buildshield_optimize_prompt(text="...", aggressiveness="medium")
|
|
52
|
+
```
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BuildShield MCP Server — stdio transport entry point.
|
|
3
|
+
|
|
4
|
+
Usage in Claude Desktop / Cursor / any MCP client:
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"mcpServers": {
|
|
8
|
+
"buildshield": {
|
|
9
|
+
"command": "buildshield-mcp",
|
|
10
|
+
"env": { "BUILDSHIELD_API_KEY": "bsk_..." }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Or with npx (npm wrapper — add later):
|
|
16
|
+
"command": "npx", "args": ["-y", "buildshield-mcp"]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import os
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from mcp.server import Server
|
|
26
|
+
from mcp.server.stdio import stdio_server
|
|
27
|
+
from mcp import types
|
|
28
|
+
|
|
29
|
+
from .tools import (
|
|
30
|
+
handle_check_cost,
|
|
31
|
+
handle_recommend_model,
|
|
32
|
+
handle_get_pricing,
|
|
33
|
+
handle_optimize_prompt,
|
|
34
|
+
handle_check_budget,
|
|
35
|
+
handle_log_usage,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
app = Server("buildshield-mcp")
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Tool schemas
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
TOOLS: list[types.Tool] = [
|
|
45
|
+
types.Tool(
|
|
46
|
+
name="buildshield_check_cost",
|
|
47
|
+
description=(
|
|
48
|
+
"Calculate the exact USD cost for a specific model and token count. "
|
|
49
|
+
"Works offline — no API key needed. "
|
|
50
|
+
"Use this before making expensive LLM calls to verify cost."
|
|
51
|
+
),
|
|
52
|
+
inputSchema={
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"model": {"type": "string", "description": "Model name, e.g. 'gpt-4.1', 'claude-sonnet-4.6', 'gemini-2.5-flash'"},
|
|
56
|
+
"input_tokens": {"type": "integer", "description": "Number of input/prompt tokens"},
|
|
57
|
+
"output_tokens": {"type": "integer", "description": "Number of output/completion tokens (default 0)", "default": 0},
|
|
58
|
+
},
|
|
59
|
+
"required": ["model", "input_tokens"],
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
types.Tool(
|
|
63
|
+
name="buildshield_recommend_model",
|
|
64
|
+
description=(
|
|
65
|
+
"Get the best model recommendations for a task, filtered by cost and context window. "
|
|
66
|
+
"Works offline. Returns a ranked table with pricing."
|
|
67
|
+
),
|
|
68
|
+
inputSchema={
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"task": {"type": "string", "description": "What you're using the model for, e.g. 'code review', 'summarization', 'complex reasoning'"},
|
|
72
|
+
"max_cost_per_1m_input": {"type": "number", "description": "Maximum acceptable input cost per 1M tokens (USD)"},
|
|
73
|
+
"min_context_tokens": {"type": "integer", "description": "Minimum context window required (tokens)"},
|
|
74
|
+
"provider": {"type": "string", "description": "Filter to a specific provider: openai, anthropic, google, deepseek"},
|
|
75
|
+
},
|
|
76
|
+
"required": ["task"],
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
types.Tool(
|
|
80
|
+
name="buildshield_get_pricing",
|
|
81
|
+
description=(
|
|
82
|
+
"Get current pricing for one model or browse all models. "
|
|
83
|
+
"Works offline. Returns input/output price per 1M tokens, context window, and tier."
|
|
84
|
+
),
|
|
85
|
+
inputSchema={
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"model": {"type": "string", "description": "Specific model to look up (optional — omit to see all models)"},
|
|
89
|
+
"provider": {"type": "string", "description": "Filter by provider: openai, anthropic, google, deepseek"},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
types.Tool(
|
|
94
|
+
name="buildshield_optimize_prompt",
|
|
95
|
+
description=(
|
|
96
|
+
"Compress a prompt using rule-based techniques to reduce token count by 20-40%. "
|
|
97
|
+
"No AI required — works completely offline. "
|
|
98
|
+
"Removes filler phrases, normalizes whitespace, deduplicates repeated instructions."
|
|
99
|
+
),
|
|
100
|
+
inputSchema={
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {
|
|
103
|
+
"text": {"type": "string", "description": "The prompt text to optimize"},
|
|
104
|
+
"aggressiveness": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"enum": ["light", "medium", "heavy"],
|
|
107
|
+
"description": "Compression level: light=whitespace only, medium=filler phrases, heavy=deduplication (default: medium)",
|
|
108
|
+
"default": "medium",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
"required": ["text"],
|
|
112
|
+
},
|
|
113
|
+
),
|
|
114
|
+
types.Tool(
|
|
115
|
+
name="buildshield_check_budget",
|
|
116
|
+
description=(
|
|
117
|
+
"Check your remaining BuildShield budget for the current period. "
|
|
118
|
+
"Requires a BuildShield API key (bsk_...) from https://thebuildshield.com/settings."
|
|
119
|
+
),
|
|
120
|
+
inputSchema={
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"api_key": {"type": "string", "description": "Your BuildShield API key (bsk_...). Falls back to BUILDSHIELD_API_KEY env var."},
|
|
124
|
+
"period": {"type": "string", "enum": ["today", "week", "month"], "description": "Budget period to check (default: today)", "default": "today"},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
types.Tool(
|
|
129
|
+
name="buildshield_log_usage",
|
|
130
|
+
description=(
|
|
131
|
+
"Log LLM usage to BuildShield for tracking and budget management. "
|
|
132
|
+
"Always calculates cost locally (offline). Syncs to dashboard if api_key provided."
|
|
133
|
+
),
|
|
134
|
+
inputSchema={
|
|
135
|
+
"type": "object",
|
|
136
|
+
"properties": {
|
|
137
|
+
"model": {"type": "string", "description": "Model used, e.g. 'gpt-4.1'"},
|
|
138
|
+
"input_tokens": {"type": "integer", "description": "Input token count"},
|
|
139
|
+
"output_tokens": {"type": "integer", "description": "Output token count"},
|
|
140
|
+
"api_key": {"type": "string", "description": "BuildShield API key to sync to dashboard (optional)"},
|
|
141
|
+
"task_description": {"type": "string", "description": "Brief description of the task for your records (optional)"},
|
|
142
|
+
},
|
|
143
|
+
"required": ["model", "input_tokens", "output_tokens"],
|
|
144
|
+
},
|
|
145
|
+
),
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# MCP handlers
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
@app.list_tools()
|
|
154
|
+
async def list_tools() -> list[types.Tool]:
|
|
155
|
+
return TOOLS
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.call_tool()
|
|
159
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
|
|
160
|
+
try:
|
|
161
|
+
if name == "buildshield_check_cost":
|
|
162
|
+
result = handle_check_cost(
|
|
163
|
+
model=arguments["model"],
|
|
164
|
+
input_tokens=int(arguments["input_tokens"]),
|
|
165
|
+
output_tokens=int(arguments.get("output_tokens", 0)),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
elif name == "buildshield_recommend_model":
|
|
169
|
+
result = handle_recommend_model(
|
|
170
|
+
task=arguments["task"],
|
|
171
|
+
max_cost_per_1m_input=arguments.get("max_cost_per_1m_input"),
|
|
172
|
+
min_context_tokens=arguments.get("min_context_tokens"),
|
|
173
|
+
provider=arguments.get("provider"),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
elif name == "buildshield_get_pricing":
|
|
177
|
+
result = handle_get_pricing(
|
|
178
|
+
model=arguments.get("model"),
|
|
179
|
+
provider=arguments.get("provider"),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
elif name == "buildshield_optimize_prompt":
|
|
183
|
+
result = handle_optimize_prompt(
|
|
184
|
+
text=arguments["text"],
|
|
185
|
+
aggressiveness=arguments.get("aggressiveness", "medium"),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
elif name == "buildshield_check_budget":
|
|
189
|
+
api_key = arguments.get("api_key") or os.environ.get("BUILDSHIELD_API_KEY")
|
|
190
|
+
result = await handle_check_budget(
|
|
191
|
+
api_key=api_key,
|
|
192
|
+
period=arguments.get("period", "today"),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
elif name == "buildshield_log_usage":
|
|
196
|
+
api_key = arguments.get("api_key") or os.environ.get("BUILDSHIELD_API_KEY")
|
|
197
|
+
result = await handle_log_usage(
|
|
198
|
+
model=arguments["model"],
|
|
199
|
+
input_tokens=int(arguments["input_tokens"]),
|
|
200
|
+
output_tokens=int(arguments["output_tokens"]),
|
|
201
|
+
api_key=api_key,
|
|
202
|
+
task_description=arguments.get("task_description"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
result = f"Unknown tool: {name}"
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
result = f"Error running {name}: {e}"
|
|
210
|
+
|
|
211
|
+
return [types.TextContent(type="text", text=result)]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Entry point
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
async def main() -> None:
|
|
219
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
220
|
+
await app.run(
|
|
221
|
+
read_stream,
|
|
222
|
+
write_stream,
|
|
223
|
+
app.create_initialization_options(),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main_sync() -> None:
|
|
228
|
+
asyncio.run(main())
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
main_sync()
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BuildShield MCP Tool Handlers
|
|
3
|
+
|
|
4
|
+
All offline tools (check_cost, recommend_model, get_pricing, optimize_prompt)
|
|
5
|
+
work with zero network calls using the embedded pricing table.
|
|
6
|
+
|
|
7
|
+
Online tools (check_budget, log_usage) require a BuildShield API key (bsk_...)
|
|
8
|
+
which users can obtain at https://thebuildshield.com.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Try to import the live pricing engine from the repo backend
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
_BACKEND_PATH = Path(__file__).parent.parent.parent.parent / "backend"
|
|
22
|
+
_PRICING_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
if _BACKEND_PATH.exists():
|
|
25
|
+
sys.path.insert(0, str(_BACKEND_PATH))
|
|
26
|
+
try:
|
|
27
|
+
from app.modules.llm_cost.pricing import ( # type: ignore[import]
|
|
28
|
+
get_pricing_table,
|
|
29
|
+
resolve_model,
|
|
30
|
+
calculate_cost,
|
|
31
|
+
)
|
|
32
|
+
_PRICING_AVAILABLE = True
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Embedded fallback — used when running standalone outside the repo
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
_FALLBACK_MODELS: list[dict] = [
|
|
40
|
+
{"provider": "openai", "model": "gpt-4.1", "display_name": "GPT-4.1", "input_per_1m": 2.00, "output_per_1m": 8.00, "context": 1_000_000, "tier": "balanced"},
|
|
41
|
+
{"provider": "openai", "model": "gpt-4.1-mini", "display_name": "GPT-4.1 Mini", "input_per_1m": 0.40, "output_per_1m": 1.60, "context": 1_000_000, "tier": "cheap"},
|
|
42
|
+
{"provider": "openai", "model": "gpt-4o", "display_name": "GPT-4o", "input_per_1m": 2.50, "output_per_1m": 10.00, "context": 128_000, "tier": "balanced"},
|
|
43
|
+
{"provider": "openai", "model": "gpt-4o-mini", "display_name": "GPT-4o Mini", "input_per_1m": 0.15, "output_per_1m": 0.60, "context": 128_000, "tier": "cheap"},
|
|
44
|
+
{"provider": "openai", "model": "o1", "display_name": "o1", "input_per_1m": 15.00, "output_per_1m": 60.00, "context": 200_000, "tier": "premium"},
|
|
45
|
+
{"provider": "openai", "model": "o3-mini", "display_name": "o3-mini", "input_per_1m": 1.10, "output_per_1m": 4.40, "context": 200_000, "tier": "balanced"},
|
|
46
|
+
{"provider": "anthropic", "model": "claude-opus-4.6", "display_name": "Claude Opus 4.6", "input_per_1m": 15.00, "output_per_1m": 75.00, "context": 200_000, "tier": "premium"},
|
|
47
|
+
{"provider": "anthropic", "model": "claude-sonnet-4.6", "display_name": "Claude Sonnet 4.6", "input_per_1m": 3.00, "output_per_1m": 15.00, "context": 200_000, "tier": "balanced"},
|
|
48
|
+
{"provider": "anthropic", "model": "claude-sonnet-4.5", "display_name": "Claude Sonnet 4.5", "input_per_1m": 3.00, "output_per_1m": 15.00, "context": 200_000, "tier": "balanced"},
|
|
49
|
+
{"provider": "anthropic", "model": "claude-haiku-4.5", "display_name": "Claude Haiku 4.5", "input_per_1m": 1.00, "output_per_1m": 5.00, "context": 200_000, "tier": "cheap"},
|
|
50
|
+
{"provider": "google", "model": "gemini-2.5-pro", "display_name": "Gemini 2.5 Pro", "input_per_1m": 1.25, "output_per_1m": 10.00, "context": 1_000_000, "tier": "balanced"},
|
|
51
|
+
{"provider": "google", "model": "gemini-2.5-flash", "display_name": "Gemini 2.5 Flash", "input_per_1m": 0.30, "output_per_1m": 2.50, "context": 1_000_000, "tier": "cheap"},
|
|
52
|
+
{"provider": "google", "model": "gemini-2.0-flash", "display_name": "Gemini 2.0 Flash", "input_per_1m": 0.10, "output_per_1m": 0.40, "context": 1_000_000, "tier": "cheap"},
|
|
53
|
+
{"provider": "deepseek", "model": "deepseek-chat", "display_name": "DeepSeek Chat", "input_per_1m": 0.28, "output_per_1m": 0.42, "context": 128_000, "tier": "cheap"},
|
|
54
|
+
{"provider": "deepseek", "model": "deepseek-r1", "display_name": "DeepSeek R1", "input_per_1m": 0.55, "output_per_1m": 2.19, "context": 128_000, "tier": "cheap"},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
_API_BASE = "https://thebuildshieldcom-production.up.railway.app"
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Helpers
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def _fmt_tokens(n: int) -> str:
|
|
64
|
+
if n >= 1_000_000:
|
|
65
|
+
return f"{n/1_000_000:.1f}M"
|
|
66
|
+
if n >= 1_000:
|
|
67
|
+
return f"{n/1_000:.0f}K"
|
|
68
|
+
return str(n)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_fallback_table() -> list[dict]:
|
|
72
|
+
return _FALLBACK_MODELS
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _fallback_resolve(model_name: str) -> dict | None:
|
|
76
|
+
name_lower = model_name.lower().strip()
|
|
77
|
+
for m in _FALLBACK_MODELS:
|
|
78
|
+
if m["model"] == name_lower or m["display_name"].lower() == name_lower:
|
|
79
|
+
return m
|
|
80
|
+
# Partial match
|
|
81
|
+
for m in _FALLBACK_MODELS:
|
|
82
|
+
if name_lower in m["model"] or m["model"] in name_lower:
|
|
83
|
+
return m
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _fallback_calculate(model_name: str, input_tokens: int, output_tokens: int) -> float | None:
|
|
88
|
+
m = _fallback_resolve(model_name)
|
|
89
|
+
if not m:
|
|
90
|
+
return None
|
|
91
|
+
return (input_tokens / 1_000_000) * m["input_per_1m"] + (output_tokens / 1_000_000) * m["output_per_1m"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Tool 1: buildshield_check_cost
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def handle_check_cost(model: str, input_tokens: int, output_tokens: int = 0) -> str:
|
|
99
|
+
"""Calculate exact cost for a specific model and token count. Works offline."""
|
|
100
|
+
if _PRICING_AVAILABLE:
|
|
101
|
+
mp = resolve_model(model)
|
|
102
|
+
if mp:
|
|
103
|
+
input_cost = (input_tokens / 1_000_000) * mp.input_per_1m
|
|
104
|
+
output_cost = (output_tokens / 1_000_000) * mp.output_per_1m
|
|
105
|
+
total = input_cost + output_cost
|
|
106
|
+
return (
|
|
107
|
+
f"Model: {mp.display_name} ({mp.provider})\n"
|
|
108
|
+
f"Tier: {mp.family}\n"
|
|
109
|
+
f"Input: {_fmt_tokens(input_tokens)} tokens × ${mp.input_per_1m:.2f}/1M = ${input_cost:.6f}\n"
|
|
110
|
+
f"Output: {_fmt_tokens(output_tokens)} tokens × ${mp.output_per_1m:.2f}/1M = ${output_cost:.6f}\n"
|
|
111
|
+
f"Total: ${total:.6f} (${total*100:.4f}¢)\n"
|
|
112
|
+
f"\nContext window: {_fmt_tokens(mp.max_context_tokens)}"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
m = _fallback_resolve(model)
|
|
116
|
+
if m:
|
|
117
|
+
input_cost = (input_tokens / 1_000_000) * m["input_per_1m"]
|
|
118
|
+
output_cost = (output_tokens / 1_000_000) * m["output_per_1m"]
|
|
119
|
+
total = input_cost + output_cost
|
|
120
|
+
return (
|
|
121
|
+
f"Model: {m['display_name']} ({m['provider']})\n"
|
|
122
|
+
f"Tier: {m['tier']}\n"
|
|
123
|
+
f"Input: {_fmt_tokens(input_tokens)} tokens × ${m['input_per_1m']:.2f}/1M = ${input_cost:.6f}\n"
|
|
124
|
+
f"Output: {_fmt_tokens(output_tokens)} tokens × ${m['output_per_1m']:.2f}/1M = ${output_cost:.6f}\n"
|
|
125
|
+
f"Total: ${total:.6f} (${total*100:.4f}¢)\n"
|
|
126
|
+
f"\nContext window: {_fmt_tokens(m['context'])}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
f"Model '{model}' not found in BuildShield pricing registry.\n"
|
|
131
|
+
f"Try: gpt-4.1, gpt-4o, claude-sonnet-4.6, gemini-2.5-flash, deepseek-chat\n"
|
|
132
|
+
f"Or use buildshield_get_pricing to browse all available models."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Tool 2: buildshield_recommend_model
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def handle_recommend_model(
|
|
141
|
+
task: str,
|
|
142
|
+
max_cost_per_1m_input: float | None = None,
|
|
143
|
+
min_context_tokens: int | None = None,
|
|
144
|
+
provider: str | None = None,
|
|
145
|
+
) -> str:
|
|
146
|
+
"""Recommend the best models for a task based on cost and capability filters. Works offline."""
|
|
147
|
+
if _PRICING_AVAILABLE:
|
|
148
|
+
table = get_pricing_table()
|
|
149
|
+
candidates = list(table.values())
|
|
150
|
+
|
|
151
|
+
def to_dict(mp: Any) -> dict:
|
|
152
|
+
return {
|
|
153
|
+
"model": mp.model,
|
|
154
|
+
"display_name": mp.display_name,
|
|
155
|
+
"provider": mp.provider,
|
|
156
|
+
"input_per_1m": mp.input_per_1m,
|
|
157
|
+
"output_per_1m": mp.output_per_1m,
|
|
158
|
+
"context": mp.max_context_tokens,
|
|
159
|
+
"tier": mp.family,
|
|
160
|
+
"status": mp.status,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
models = [to_dict(m) for m in candidates if m.status != "legacy"]
|
|
164
|
+
else:
|
|
165
|
+
models = [m for m in _FALLBACK_MODELS]
|
|
166
|
+
|
|
167
|
+
# Apply filters
|
|
168
|
+
if max_cost_per_1m_input is not None:
|
|
169
|
+
models = [m for m in models if m["input_per_1m"] <= max_cost_per_1m_input]
|
|
170
|
+
if min_context_tokens is not None:
|
|
171
|
+
models = [m for m in models if m["context"] >= min_context_tokens]
|
|
172
|
+
if provider:
|
|
173
|
+
models = [m for m in models if m["provider"].lower() == provider.lower()]
|
|
174
|
+
|
|
175
|
+
# Sort by input cost
|
|
176
|
+
models.sort(key=lambda m: m["input_per_1m"])
|
|
177
|
+
|
|
178
|
+
if not models:
|
|
179
|
+
return (
|
|
180
|
+
"No models match your filters.\n"
|
|
181
|
+
"Try relaxing: max_cost_per_1m_input or min_context_tokens.\n"
|
|
182
|
+
"Use buildshield_get_pricing to see all available models."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Build recommendation
|
|
186
|
+
lines = [f"Top model recommendations for: {task}\n"]
|
|
187
|
+
lines.append(f"{'Model':<30} {'Provider':<12} {'Input/1M':>10} {'Output/1M':>11} {'Context':>10} {'Tier':<10}")
|
|
188
|
+
lines.append("─" * 85)
|
|
189
|
+
|
|
190
|
+
for m in models[:6]:
|
|
191
|
+
ctx = _fmt_tokens(m["context"])
|
|
192
|
+
lines.append(
|
|
193
|
+
f"{m['display_name']:<30} {m['provider']:<12} ${m['input_per_1m']:>8.2f} "
|
|
194
|
+
f"${m['output_per_1m']:>9.2f} {ctx:>10} {m['tier']:<10}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Add a recommendation note based on task keywords
|
|
198
|
+
task_lower = task.lower()
|
|
199
|
+
note = ""
|
|
200
|
+
if any(w in task_lower for w in ["summar", "classif", "extract", "simple", "short"]):
|
|
201
|
+
note = "\n💡 For simple extraction/classification tasks, consider the cheapest option — quality difference is minimal."
|
|
202
|
+
elif any(w in task_lower for w in ["code", "debug", "program", "function"]):
|
|
203
|
+
note = "\n💡 For coding tasks, balanced-tier models (Sonnet, GPT-4.1) offer the best quality/cost ratio."
|
|
204
|
+
elif any(w in task_lower for w in ["reason", "complex", "analyz", "research"]):
|
|
205
|
+
note = "\n💡 For complex reasoning, premium models (Opus, o1) justify their cost with significantly better output."
|
|
206
|
+
elif any(w in task_lower for w in ["long", "document", "book", "context"]):
|
|
207
|
+
note = "\n💡 For long-context tasks, Gemini 2.5 Pro (1M context) or Claude (200K) are the best choices."
|
|
208
|
+
|
|
209
|
+
return "\n".join(lines) + note
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
# Tool 3: buildshield_get_pricing
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
def handle_get_pricing(model: str | None = None, provider: str | None = None) -> str:
|
|
217
|
+
"""Get pricing for one model or browse all models. Works offline."""
|
|
218
|
+
if model:
|
|
219
|
+
if _PRICING_AVAILABLE:
|
|
220
|
+
mp = resolve_model(model)
|
|
221
|
+
if mp:
|
|
222
|
+
return (
|
|
223
|
+
f"Model: {mp.display_name}\n"
|
|
224
|
+
f"Provider: {mp.provider}\n"
|
|
225
|
+
f"Model ID: {mp.model}\n"
|
|
226
|
+
f"Input price: ${mp.input_per_1m:.2f} per 1M tokens\n"
|
|
227
|
+
f"Output price: ${mp.output_per_1m:.2f} per 1M tokens\n"
|
|
228
|
+
f"Context: {_fmt_tokens(mp.max_context_tokens)}\n"
|
|
229
|
+
f"Tier: {mp.family}\n"
|
|
230
|
+
f"Status: {mp.status}\n"
|
|
231
|
+
+ (f"Alternatives: {', '.join(mp.alternatives)}" if mp.alternatives else "")
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
m = _fallback_resolve(model)
|
|
235
|
+
if m:
|
|
236
|
+
return (
|
|
237
|
+
f"Model: {m['display_name']}\n"
|
|
238
|
+
f"Provider: {m['provider']}\n"
|
|
239
|
+
f"Model ID: {m['model']}\n"
|
|
240
|
+
f"Input price: ${m['input_per_1m']:.2f} per 1M tokens\n"
|
|
241
|
+
f"Output price: ${m['output_per_1m']:.2f} per 1M tokens\n"
|
|
242
|
+
f"Context: {_fmt_tokens(m['context'])}\n"
|
|
243
|
+
f"Tier: {m['tier']}"
|
|
244
|
+
)
|
|
245
|
+
return f"Model '{model}' not found. Use buildshield_get_pricing (no model arg) to see all models."
|
|
246
|
+
|
|
247
|
+
# Return full table
|
|
248
|
+
if _PRICING_AVAILABLE:
|
|
249
|
+
table = get_pricing_table()
|
|
250
|
+
models = sorted(table.values(), key=lambda m: (m.provider, m.input_per_1m))
|
|
251
|
+
rows = []
|
|
252
|
+
for mp in models:
|
|
253
|
+
if provider and mp.provider.lower() != provider.lower():
|
|
254
|
+
continue
|
|
255
|
+
rows.append({
|
|
256
|
+
"display_name": mp.display_name,
|
|
257
|
+
"provider": mp.provider,
|
|
258
|
+
"input_per_1m": mp.input_per_1m,
|
|
259
|
+
"output_per_1m": mp.output_per_1m,
|
|
260
|
+
"context": mp.max_context_tokens,
|
|
261
|
+
"tier": mp.family,
|
|
262
|
+
})
|
|
263
|
+
else:
|
|
264
|
+
rows = sorted(
|
|
265
|
+
[m for m in _FALLBACK_MODELS if not provider or m["provider"].lower() == provider.lower()],
|
|
266
|
+
key=lambda m: (m["provider"], m["input_per_1m"])
|
|
267
|
+
)
|
|
268
|
+
rows = [dict(display_name=m["display_name"], provider=m["provider"],
|
|
269
|
+
input_per_1m=m["input_per_1m"], output_per_1m=m["output_per_1m"],
|
|
270
|
+
context=m["context"], tier=m["tier"]) for m in rows]
|
|
271
|
+
|
|
272
|
+
if not rows:
|
|
273
|
+
return f"No models found for provider '{provider}'."
|
|
274
|
+
|
|
275
|
+
lines = [f"{'Model':<30} {'Provider':<12} {'Input/1M':>10} {'Output/1M':>11} {'Context':>10} {'Tier':<10}"]
|
|
276
|
+
lines.append("─" * 85)
|
|
277
|
+
for r in rows:
|
|
278
|
+
ctx = _fmt_tokens(r["context"])
|
|
279
|
+
lines.append(
|
|
280
|
+
f"{r['display_name']:<30} {r['provider']:<12} ${r['input_per_1m']:>8.2f} "
|
|
281
|
+
f"${r['output_per_1m']:>9.2f} {ctx:>10} {r['tier']:<10}"
|
|
282
|
+
)
|
|
283
|
+
lines.append(f"\n{len(rows)} models listed.")
|
|
284
|
+
return "\n".join(lines)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# Tool 4: buildshield_optimize_prompt
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
_FILLER_REPLACEMENTS = [
|
|
292
|
+
(r"\bplease\s+", ""),
|
|
293
|
+
(r"\bkindly\s+", ""),
|
|
294
|
+
(r"\bI would like you to\s+", ""),
|
|
295
|
+
(r"\bmake sure to\s+", "ensure "),
|
|
296
|
+
(r"\bin order to\b", "to"),
|
|
297
|
+
(r"\bas well as\b", "and"),
|
|
298
|
+
(r"\bin the event that\b", "if"),
|
|
299
|
+
(r"\bat this point in time\b", "now"),
|
|
300
|
+
(r"\bdue to the fact that\b", "because"),
|
|
301
|
+
(r"\bfor the purpose of\b", "for"),
|
|
302
|
+
(r"\bit is important to note that\b", "Note:"),
|
|
303
|
+
(r"\bplease note that\b", "Note:"),
|
|
304
|
+
(r"\bit should be noted that\b", "Note:"),
|
|
305
|
+
(r"\bfeel free to\b", ""),
|
|
306
|
+
(r"\bdo not hesitate to\b", ""),
|
|
307
|
+
(r"\bI would appreciate it if you could\b", ""),
|
|
308
|
+
(r"\bwould you be able to\b", ""),
|
|
309
|
+
(r"\bcould you please\b", ""),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _word_overlap(a: str, b: str) -> float:
|
|
314
|
+
wa, wb = set(a.lower().split()), set(b.lower().split())
|
|
315
|
+
if not wa or not wb:
|
|
316
|
+
return 0.0
|
|
317
|
+
return len(wa & wb) / max(len(wa), len(wb))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _dedup_paragraphs(text: str) -> tuple[str, int]:
|
|
321
|
+
paras = [p.strip() for p in re.split(r"\n{2,}", text) if p.strip()]
|
|
322
|
+
removed = 0
|
|
323
|
+
result = []
|
|
324
|
+
for i, para in enumerate(paras):
|
|
325
|
+
is_dup = False
|
|
326
|
+
for j in range(i + 1, len(paras)):
|
|
327
|
+
if _word_overlap(para, paras[j]) > 0.60:
|
|
328
|
+
is_dup = True
|
|
329
|
+
removed += 1
|
|
330
|
+
break
|
|
331
|
+
if not is_dup:
|
|
332
|
+
result.append(para)
|
|
333
|
+
return "\n\n".join(result), removed
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def handle_optimize_prompt(text: str, aggressiveness: str = "medium") -> str:
|
|
337
|
+
"""Rule-based prompt compression. No AI — works offline. Saves 20-40% on verbose prompts."""
|
|
338
|
+
if not text or not text.strip():
|
|
339
|
+
return "Empty prompt provided. Please pass the text you want to optimize."
|
|
340
|
+
|
|
341
|
+
original_tokens = max(1, len(text) // 4)
|
|
342
|
+
techniques: list[str] = []
|
|
343
|
+
optimized = text
|
|
344
|
+
|
|
345
|
+
# ---- Light: whitespace normalization ----
|
|
346
|
+
before = optimized
|
|
347
|
+
optimized = re.sub(r"[ \t]+", " ", optimized) # collapse spaces/tabs
|
|
348
|
+
optimized = re.sub(r"\n{3,}", "\n\n", optimized) # max 2 blank lines
|
|
349
|
+
optimized = "\n".join(line.rstrip() for line in optimized.splitlines())
|
|
350
|
+
optimized = optimized.strip()
|
|
351
|
+
if optimized != before:
|
|
352
|
+
techniques.append("Whitespace normalization")
|
|
353
|
+
|
|
354
|
+
if aggressiveness in ("medium", "heavy"):
|
|
355
|
+
# ---- Medium: filler phrase removal ----
|
|
356
|
+
before = optimized
|
|
357
|
+
for pattern, replacement in _FILLER_REPLACEMENTS:
|
|
358
|
+
optimized = re.sub(pattern, replacement, optimized, flags=re.IGNORECASE)
|
|
359
|
+
# Clean up double spaces left by removals
|
|
360
|
+
optimized = re.sub(r" +", " ", optimized)
|
|
361
|
+
optimized = re.sub(r"^ +", "", optimized, flags=re.MULTILINE)
|
|
362
|
+
if optimized != before:
|
|
363
|
+
techniques.append("Filler phrase removal")
|
|
364
|
+
|
|
365
|
+
if aggressiveness == "heavy":
|
|
366
|
+
# ---- Heavy: paragraph deduplication ----
|
|
367
|
+
before = optimized
|
|
368
|
+
optimized, removed_count = _dedup_paragraphs(optimized)
|
|
369
|
+
if removed_count:
|
|
370
|
+
techniques.append(f"Duplicate paragraph removal ({removed_count} removed)")
|
|
371
|
+
|
|
372
|
+
optimized_tokens = max(1, len(optimized) // 4)
|
|
373
|
+
savings_pct = round((1 - optimized_tokens / original_tokens) * 100, 1)
|
|
374
|
+
|
|
375
|
+
if not techniques:
|
|
376
|
+
return (
|
|
377
|
+
f"Prompt analyzed — no optimizations needed.\n"
|
|
378
|
+
f"Tokens: {original_tokens:,} | The prompt is already concise."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Format output
|
|
382
|
+
lines = [
|
|
383
|
+
f"Optimization complete ({aggressiveness} mode)",
|
|
384
|
+
f"",
|
|
385
|
+
f"Original: {original_tokens:,} tokens",
|
|
386
|
+
f"Optimized: {optimized_tokens:,} tokens",
|
|
387
|
+
f"Saved: {original_tokens - optimized_tokens:,} tokens ({savings_pct}%)",
|
|
388
|
+
f"",
|
|
389
|
+
f"Techniques applied:",
|
|
390
|
+
]
|
|
391
|
+
for t in techniques:
|
|
392
|
+
lines.append(f" • {t}")
|
|
393
|
+
lines += [
|
|
394
|
+
f"",
|
|
395
|
+
f"--- OPTIMIZED PROMPT ---",
|
|
396
|
+
optimized,
|
|
397
|
+
f"--- END ---",
|
|
398
|
+
]
|
|
399
|
+
return "\n".join(lines)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
# Tool 5: buildshield_check_budget
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
async def handle_check_budget(api_key: str | None = None, period: str = "today") -> str:
|
|
407
|
+
"""Check your BuildShield budget. Requires a BuildShield API key (bsk_...)."""
|
|
408
|
+
if not api_key:
|
|
409
|
+
return (
|
|
410
|
+
"This tool requires a BuildShield API key.\n\n"
|
|
411
|
+
"Get your key at: https://thebuildshield.com/settings\n"
|
|
412
|
+
"Then pass it as: api_key='bsk_...'\n\n"
|
|
413
|
+
"Or configure it via the BUILDSHIELD_API_KEY environment variable."
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if not api_key.startswith("bsk_"):
|
|
417
|
+
return f"Invalid key format. BuildShield keys start with 'bsk_'. Got: '{api_key[:8]}...'"
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
import httpx
|
|
421
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
422
|
+
r = await client.get(
|
|
423
|
+
f"{_API_BASE}/api/v1/agent/budget",
|
|
424
|
+
params={"period": period},
|
|
425
|
+
headers={"X-Api-Key": api_key},
|
|
426
|
+
)
|
|
427
|
+
if r.status_code == 401:
|
|
428
|
+
return "Invalid or expired API key. Check your key at https://thebuildshield.com/settings"
|
|
429
|
+
if r.status_code == 404:
|
|
430
|
+
return "No budget configured for this key. Set a budget at https://thebuildshield.com/settings"
|
|
431
|
+
r.raise_for_status()
|
|
432
|
+
data = r.json()
|
|
433
|
+
budget = data.get("budget", {})
|
|
434
|
+
quota = data.get("quota", {})
|
|
435
|
+
limit = budget.get("limit_usd") or 0
|
|
436
|
+
used = budget.get("used_usd") or 0
|
|
437
|
+
remaining = budget.get("remaining_usd")
|
|
438
|
+
if remaining is None:
|
|
439
|
+
remaining = limit - used
|
|
440
|
+
resets = budget.get("reset_at") or "not set"
|
|
441
|
+
calls_used = quota.get("calls_used_this_month", 0)
|
|
442
|
+
calls_limit = quota.get("calls_limit")
|
|
443
|
+
pct = round((used / limit * 100) if limit else 0, 1)
|
|
444
|
+
bar_filled = int(pct / 5)
|
|
445
|
+
bar = "█" * bar_filled + "░" * (20 - bar_filled)
|
|
446
|
+
plan = data.get("plan", "unknown")
|
|
447
|
+
quota_line = f"API calls: {calls_used}/{calls_limit if calls_limit else '∞'} this month"
|
|
448
|
+
if budget.get("has_budget"):
|
|
449
|
+
return (
|
|
450
|
+
f"Budget Status ({period}) — Plan: {plan}\n"
|
|
451
|
+
f"{'─'*30}\n"
|
|
452
|
+
f"Used: ${used:.4f} / ${limit:.2f}\n"
|
|
453
|
+
f"Remaining: ${remaining:.4f}\n"
|
|
454
|
+
f"Progress: [{bar}] {pct}%\n"
|
|
455
|
+
f"Resets at: {resets}\n"
|
|
456
|
+
f"{quota_line}"
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
return (
|
|
460
|
+
f"Budget Status — Plan: {plan}\n"
|
|
461
|
+
f"{'─'*30}\n"
|
|
462
|
+
f"No spending budget configured.\n"
|
|
463
|
+
f"Set a budget at: https://thebuildshield.com/settings\n"
|
|
464
|
+
f"{quota_line}"
|
|
465
|
+
)
|
|
466
|
+
except ImportError:
|
|
467
|
+
return "httpx not installed. Run: pip install httpx"
|
|
468
|
+
except Exception as e:
|
|
469
|
+
return f"Could not reach BuildShield API: {e}\nCheck your connection or try again later."
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
# Tool 6: buildshield_log_usage
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
async def handle_log_usage(
|
|
477
|
+
model: str,
|
|
478
|
+
input_tokens: int,
|
|
479
|
+
output_tokens: int,
|
|
480
|
+
api_key: str | None = None,
|
|
481
|
+
task_description: str | None = None,
|
|
482
|
+
) -> str:
|
|
483
|
+
"""Log AI usage to BuildShield. Always calculates cost locally; syncs to cloud if api_key provided."""
|
|
484
|
+
# Calculate cost locally first (always works offline)
|
|
485
|
+
if _PRICING_AVAILABLE:
|
|
486
|
+
cost = calculate_cost(model, input_tokens, output_tokens)
|
|
487
|
+
mp = resolve_model(model)
|
|
488
|
+
model_display = mp.display_name if mp else model
|
|
489
|
+
else:
|
|
490
|
+
cost = _fallback_calculate(model, input_tokens, output_tokens)
|
|
491
|
+
m = _fallback_resolve(model)
|
|
492
|
+
model_display = m["display_name"] if m else model
|
|
493
|
+
|
|
494
|
+
if cost is None:
|
|
495
|
+
cost_str = "(model not found — cost unknown)"
|
|
496
|
+
cost = 0.0
|
|
497
|
+
else:
|
|
498
|
+
cost_str = f"${cost:.6f}"
|
|
499
|
+
|
|
500
|
+
local_summary = (
|
|
501
|
+
f"Usage logged locally\n"
|
|
502
|
+
f"Model: {model_display}\n"
|
|
503
|
+
f"Input: {input_tokens:,} tokens\n"
|
|
504
|
+
f"Output: {output_tokens:,} tokens\n"
|
|
505
|
+
f"Cost: {cost_str}"
|
|
506
|
+
)
|
|
507
|
+
if task_description:
|
|
508
|
+
local_summary += f"\nTask: {task_description}"
|
|
509
|
+
|
|
510
|
+
if not api_key:
|
|
511
|
+
return local_summary + "\n\nNote: Pass api_key='bsk_...' to sync to your BuildShield dashboard."
|
|
512
|
+
|
|
513
|
+
# Sync to API
|
|
514
|
+
try:
|
|
515
|
+
import httpx
|
|
516
|
+
payload = {
|
|
517
|
+
"model": model,
|
|
518
|
+
"input_tokens": input_tokens,
|
|
519
|
+
"output_tokens": output_tokens,
|
|
520
|
+
"estimated_cost_usd": cost,
|
|
521
|
+
"source": "mcp_server",
|
|
522
|
+
}
|
|
523
|
+
if task_description:
|
|
524
|
+
payload["task_description"] = task_description
|
|
525
|
+
|
|
526
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
527
|
+
r = await client.post(
|
|
528
|
+
f"{_API_BASE}/api/v1/agent/usage",
|
|
529
|
+
json=payload,
|
|
530
|
+
headers={"X-Api-Key": api_key},
|
|
531
|
+
)
|
|
532
|
+
if r.status_code in (200, 201):
|
|
533
|
+
data = r.json()
|
|
534
|
+
remaining = data.get("budget_remaining_usd")
|
|
535
|
+
synced_note = "✓ Synced to BuildShield dashboard"
|
|
536
|
+
if remaining is not None:
|
|
537
|
+
synced_note += f" | Budget remaining: ${remaining:.4f}"
|
|
538
|
+
return local_summary + f"\n\n{synced_note}"
|
|
539
|
+
elif r.status_code == 429:
|
|
540
|
+
return local_summary + "\n\n⚠ Budget limit reached — usage logged locally only."
|
|
541
|
+
else:
|
|
542
|
+
return local_summary + f"\n\nCloud sync failed (HTTP {r.status_code}) — usage logged locally."
|
|
543
|
+
except ImportError:
|
|
544
|
+
return local_summary + "\n\nhttpx not installed. Run: pip install httpx"
|
|
545
|
+
except Exception as e:
|
|
546
|
+
return local_summary + f"\n\nCloud sync failed: {e} — usage logged locally."
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "buildshield-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for BuildShield — AI cost intelligence for agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["mcp", "ai", "cost", "llm", "buildshield"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mcp>=1.0.0",
|
|
15
|
+
"httpx>=0.27.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
buildshield-mcp = "buildshield_mcp.server:main_sync"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://thebuildshield.com"
|
|
23
|
+
Repository = "https://github.com/Dev-31/theBuildShield.com"
|