tokenstretcher 1.0.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.
- tokensaver/__init__.py +76 -0
- tokensaver/agent.py +144 -0
- tokensaver/cli.py +354 -0
- tokensaver/config.py +129 -0
- tokensaver/email.py +212 -0
- tokensaver/key_manager.py +191 -0
- tokensaver/manager.py +420 -0
- tokensaver/mcp_server.py +30 -0
- tokensaver/models.py +221 -0
- tokensaver/prompts.py +225 -0
- tokensaver/utils.py +445 -0
- tokensaver/wallet.py +172 -0
- tokenstretcher-1.0.0.dist-info/METADATA +574 -0
- tokenstretcher-1.0.0.dist-info/RECORD +18 -0
- tokenstretcher-1.0.0.dist-info/WHEEL +5 -0
- tokenstretcher-1.0.0.dist-info/entry_points.txt +2 -0
- tokenstretcher-1.0.0.dist-info/licenses/LICENSE +76 -0
- tokenstretcher-1.0.0.dist-info/top_level.txt +1 -0
tokensaver/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TokenSaverAI — Hierarchical AI Task Manager for Token & Cost Efficiency
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
from tokensaver import Manager, TokenSaverConfig
|
|
6
|
+
from tokensaver.manager import Manager
|
|
7
|
+
from tokensaver.config import load_config
|
|
8
|
+
|
|
9
|
+
result = await Manager().run("Build a complete FastAPI auth system")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Manager",
|
|
17
|
+
"Agent",
|
|
18
|
+
"TokenSaverConfig",
|
|
19
|
+
"ManagerResult",
|
|
20
|
+
"load_config",
|
|
21
|
+
"run",
|
|
22
|
+
"run_json",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
from tokensaver.agent import Agent
|
|
26
|
+
from tokensaver.config import load_config
|
|
27
|
+
from tokensaver.manager import Manager
|
|
28
|
+
from tokensaver.manager import ManagerResult
|
|
29
|
+
from tokensaver.models import TokenSaverConfig
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def run(prompt: str, **config_overrides) -> "ManagerResult":
|
|
33
|
+
"""
|
|
34
|
+
Primary one-liner API for AI agents (Grok Build, OpenCode, etc.).
|
|
35
|
+
|
|
36
|
+
Recommended usage from another AI system:
|
|
37
|
+
|
|
38
|
+
from tokensaver import run
|
|
39
|
+
result = await run(
|
|
40
|
+
"Build a production FastAPI auth system with JWT + refresh tokens",
|
|
41
|
+
proxy_mode=False, # or True if using prepaid billing
|
|
42
|
+
verbose=False # cleaner output for agents
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
print(result.final_output)
|
|
46
|
+
print(result.savings.summary())
|
|
47
|
+
"""
|
|
48
|
+
from tokensaver.manager import ManagerResult
|
|
49
|
+
|
|
50
|
+
cfg = load_config()
|
|
51
|
+
for k, v in config_overrides.items():
|
|
52
|
+
if hasattr(cfg, k):
|
|
53
|
+
setattr(cfg, k, v)
|
|
54
|
+
|
|
55
|
+
mgr = Manager(config=cfg)
|
|
56
|
+
return await mgr.run(prompt, return_report=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def run_json(prompt: str, **config_overrides) -> dict:
|
|
60
|
+
"""
|
|
61
|
+
Convenience wrapper that returns a clean dictionary suitable for JSON serialization.
|
|
62
|
+
|
|
63
|
+
Especially useful when calling from Grok Build or other agent frameworks.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
from tokensaver import run_json
|
|
67
|
+
data = await run_json("Refactor the auth module", verbose=False)
|
|
68
|
+
print(data["savings"]["percent_saved"])
|
|
69
|
+
"""
|
|
70
|
+
result = await run(prompt, **config_overrides)
|
|
71
|
+
return {
|
|
72
|
+
"final_output": result.final_output,
|
|
73
|
+
"savings": result.savings.model_dump(),
|
|
74
|
+
"plan_reasoning": result.plan.reasoning,
|
|
75
|
+
"total_duration_seconds": result.total_duration_seconds,
|
|
76
|
+
}
|
tokensaver/agent.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Specialized Agent implementation for TokenSaverAI.
|
|
3
|
+
|
|
4
|
+
Each Agent is intentionally narrow in scope and context.
|
|
5
|
+
This is the key to massive token savings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from litellm import acompletion
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from tokensaver.models import AgentResult, ModelTier, Subtask, TaskStatus, TokenSaverConfig
|
|
17
|
+
from tokensaver.prompts import build_agent_system_prompt
|
|
18
|
+
from tokensaver.utils import (
|
|
19
|
+
call_llm_with_retry,
|
|
20
|
+
estimate_cost,
|
|
21
|
+
filter_context_for_task,
|
|
22
|
+
get_llm_call_kwargs,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Agent:
|
|
29
|
+
"""
|
|
30
|
+
A highly specialized, context-constrained AI worker.
|
|
31
|
+
|
|
32
|
+
Agents receive:
|
|
33
|
+
- A precise role + system prompt
|
|
34
|
+
- Only the minimal context they actually need
|
|
35
|
+
- A single focused task
|
|
36
|
+
|
|
37
|
+
They are cheap and fast by design.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
subtask: Subtask,
|
|
43
|
+
config: TokenSaverConfig,
|
|
44
|
+
full_project_context: str | None = None,
|
|
45
|
+
):
|
|
46
|
+
self.subtask = subtask
|
|
47
|
+
self.config = config
|
|
48
|
+
self.full_project_context = full_project_context or ""
|
|
49
|
+
|
|
50
|
+
llm_kwargs = get_llm_call_kwargs(config)
|
|
51
|
+
force_xai = llm_kwargs.get("force_xai_models", False)
|
|
52
|
+
self.model = config.get_model_for_tier(subtask.model_preference, force_xai_models=force_xai)
|
|
53
|
+
|
|
54
|
+
# Build the tight system prompt for this specialist
|
|
55
|
+
filtered_context = filter_context_for_task(
|
|
56
|
+
self.full_project_context,
|
|
57
|
+
subtask.context_keywords,
|
|
58
|
+
max_chars=4200 if subtask.model_preference != ModelTier.POWERFUL else 8500,
|
|
59
|
+
)
|
|
60
|
+
self.system_prompt = build_agent_system_prompt(subtask.role, filtered_context)
|
|
61
|
+
|
|
62
|
+
async def run(self, user_instruction_override: str | None = None) -> AgentResult:
|
|
63
|
+
"""
|
|
64
|
+
Execute the agent's assigned subtask.
|
|
65
|
+
Returns structured result with real token and cost data when available.
|
|
66
|
+
"""
|
|
67
|
+
start = time.perf_counter()
|
|
68
|
+
instruction = user_instruction_override or self.subtask.description
|
|
69
|
+
|
|
70
|
+
messages = [
|
|
71
|
+
{"role": "system", "content": self.system_prompt},
|
|
72
|
+
{"role": "user", "content": instruction},
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
llm_kwargs = get_llm_call_kwargs(self.config)
|
|
77
|
+
resp = await call_llm_with_retry(
|
|
78
|
+
acompletion,
|
|
79
|
+
model=self.model,
|
|
80
|
+
messages=messages,
|
|
81
|
+
max_tokens=2800 if self.subtask.model_preference != ModelTier.CHEAP_FAST else 1600,
|
|
82
|
+
temperature=0.2,
|
|
83
|
+
timeout=self.config.default_timeout_seconds,
|
|
84
|
+
**llm_kwargs,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
output = resp.choices[0].message.content or "(empty response)"
|
|
88
|
+
usage = getattr(resp, "usage", None)
|
|
89
|
+
|
|
90
|
+
if usage:
|
|
91
|
+
tokens = int(getattr(usage, "total_tokens", 0) or 0)
|
|
92
|
+
# LiteLLM sometimes puts cost on the response
|
|
93
|
+
cost = float(getattr(resp, "_response_cost", 0.0) or 0.0)
|
|
94
|
+
if cost == 0.0:
|
|
95
|
+
cost = estimate_cost(tokens, self.model)
|
|
96
|
+
else:
|
|
97
|
+
# Fallback estimation
|
|
98
|
+
tokens = int(len(output) / 3.5) + int(len(self.system_prompt) / 3.8) + 180
|
|
99
|
+
cost = estimate_cost(tokens, self.model)
|
|
100
|
+
|
|
101
|
+
duration = time.perf_counter() - start
|
|
102
|
+
|
|
103
|
+
if self.config.verbose:
|
|
104
|
+
console.print(
|
|
105
|
+
f"[dim] ✓ {self.subtask.id} ({self.subtask.role[:30]}) — "
|
|
106
|
+
f"{tokens:,} tokens — ${cost:.5f} — {duration:.1f}s[/dim]"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return AgentResult(
|
|
110
|
+
task_id=self.subtask.id,
|
|
111
|
+
role=self.subtask.role,
|
|
112
|
+
output=output.strip(),
|
|
113
|
+
model_used=self.model,
|
|
114
|
+
tokens_used=tokens,
|
|
115
|
+
cost_usd=round(cost, 6),
|
|
116
|
+
duration_seconds=round(duration, 2),
|
|
117
|
+
status=TaskStatus.COMPLETED,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
duration = time.perf_counter() - start
|
|
122
|
+
console.print(f"[red] ✗ Agent {self.subtask.id} failed: {exc}[/red]")
|
|
123
|
+
|
|
124
|
+
return AgentResult(
|
|
125
|
+
task_id=self.subtask.id,
|
|
126
|
+
role=self.subtask.role,
|
|
127
|
+
output="",
|
|
128
|
+
model_used=self.model,
|
|
129
|
+
tokens_used=0,
|
|
130
|
+
cost_usd=0.0,
|
|
131
|
+
duration_seconds=round(duration, 2),
|
|
132
|
+
status=TaskStatus.FAILED,
|
|
133
|
+
error=str(exc),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def run_agent(
|
|
138
|
+
subtask: Subtask,
|
|
139
|
+
config: TokenSaverConfig,
|
|
140
|
+
context: str | None = None,
|
|
141
|
+
) -> AgentResult:
|
|
142
|
+
"""Convenience wrapper."""
|
|
143
|
+
agent = Agent(subtask, config, context)
|
|
144
|
+
return await agent.run()
|
tokensaver/cli.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TokenSaverAI Command Line Interface.
|
|
3
|
+
|
|
4
|
+
Usage examples:
|
|
5
|
+
tokensaver "Build a FastAPI JWT authentication service with user management"
|
|
6
|
+
tokensaver "Design a scalable multi-tenant SaaS backend" --budget 0.80
|
|
7
|
+
tokensaver interactive
|
|
8
|
+
tokensaver --plan-only "Refactor the entire auth module"
|
|
9
|
+
|
|
10
|
+
DISCLAIMER
|
|
11
|
+
----------
|
|
12
|
+
Commands related to proxy management, virtual keys, and prepaid billing
|
|
13
|
+
are provided for convenience only. The authors accept no liability for
|
|
14
|
+
any financial, operational, or legal issues arising from commercial use
|
|
15
|
+
of these features. See LICENSE for full terms.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import asyncio
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from rich.console import Console
|
|
27
|
+
from rich.prompt import Prompt
|
|
28
|
+
|
|
29
|
+
from tokensaver.config import load_config, save_example_config
|
|
30
|
+
from tokensaver.manager import Manager
|
|
31
|
+
from tokensaver.models import TokenSaverConfig
|
|
32
|
+
from tokensaver.wallet import Wallet
|
|
33
|
+
from tokensaver.key_manager import KeyManager
|
|
34
|
+
from tokensaver.email import send_key_email
|
|
35
|
+
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
prog="tokensaver",
|
|
42
|
+
description="TokenSaverAI — Hierarchical AI task manager for maximum quality at minimum cost",
|
|
43
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Common flags that apply to task execution
|
|
47
|
+
parser.add_argument("-i", "--interactive", action="store_true", help="Launch interactive REPL mode")
|
|
48
|
+
parser.add_argument("--plan-only", action="store_true", help="Only show the decomposition plan")
|
|
49
|
+
parser.add_argument("--model", dest="powerful_model", help="Override powerful model (e.g. xai/grok-4)")
|
|
50
|
+
parser.add_argument("--budget", type=float, default=None, help="Soft max cost in USD")
|
|
51
|
+
parser.add_argument("--config", type=Path, default=None, help="Path to custom config")
|
|
52
|
+
parser.add_argument("--quiet", action="store_true", help="Reduce output")
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--json", action="store_true",
|
|
55
|
+
help="Output structured JSON (strongly recommended when called by Grok Build, OpenCode, or other AI agents)"
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument("--init-config", action="store_true", help="Write example config and exit")
|
|
58
|
+
from tokensaver import __version__
|
|
59
|
+
parser.add_argument("--version", action="version", version=f"TokenSaverAI {__version__}")
|
|
60
|
+
|
|
61
|
+
# Subcommands (wallet + proxy management)
|
|
62
|
+
subparsers = parser.add_subparsers(dest="command", help="Management commands (optional)")
|
|
63
|
+
|
|
64
|
+
# balance
|
|
65
|
+
p_balance = subparsers.add_parser("balance", help="Show current prepaid balance and spending")
|
|
66
|
+
p_balance.add_argument("--detailed", action="store_true", help="Show recent transactions")
|
|
67
|
+
|
|
68
|
+
# topup
|
|
69
|
+
p_topup = subparsers.add_parser("topup", help="Show instructions to add funds (Stripe / Lemon Squeezy)")
|
|
70
|
+
|
|
71
|
+
# add-funds
|
|
72
|
+
p_add = subparsers.add_parser("add-funds", help="Credit your local wallet (for testing or manual topups)")
|
|
73
|
+
p_add.add_argument("amount", type=float, help="Amount in USD to add to prepaid balance")
|
|
74
|
+
|
|
75
|
+
# proxy
|
|
76
|
+
p_proxy = subparsers.add_parser("proxy", help="Virtual key & proxy management")
|
|
77
|
+
proxy_sub = p_proxy.add_subparsers(dest="proxy_action")
|
|
78
|
+
proxy_sub.add_parser("start", help="Instructions to start the budget-enforcing proxy server")
|
|
79
|
+
|
|
80
|
+
p_create = proxy_sub.add_parser("create-key", help="Create a virtual key for a user and email it")
|
|
81
|
+
p_create.add_argument("email", help="User email address")
|
|
82
|
+
p_create.add_argument("budget", type=float, nargs="?", default=10.0, help="Initial budget in USD (default 10)")
|
|
83
|
+
|
|
84
|
+
p_disable = proxy_sub.add_parser("disable-key", help="Revoke a virtual key")
|
|
85
|
+
p_disable.add_argument("key", help="The virtual key to disable")
|
|
86
|
+
|
|
87
|
+
proxy_sub.add_parser("dashboard", help="Show all virtual keys and wallet status")
|
|
88
|
+
|
|
89
|
+
# config
|
|
90
|
+
subparsers.add_parser("config", help="Interactive setup for proxy mode and payment links")
|
|
91
|
+
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def run_headless(prompt: str, args: argparse.Namespace, config: TokenSaverConfig) -> None:
|
|
96
|
+
"""Execute a single prompt and print the beautiful result."""
|
|
97
|
+
if args.quiet:
|
|
98
|
+
config.verbose = False
|
|
99
|
+
|
|
100
|
+
if args.powerful_model:
|
|
101
|
+
config.default_powerful_model = args.powerful_model
|
|
102
|
+
|
|
103
|
+
if args.budget:
|
|
104
|
+
config.max_cost_usd = args.budget
|
|
105
|
+
|
|
106
|
+
mgr = Manager(config=config)
|
|
107
|
+
result = await mgr.run(prompt, return_report=True)
|
|
108
|
+
|
|
109
|
+
if args.json:
|
|
110
|
+
# Machine-friendly output for Grok Build, OpenCode, Cursor agents, etc.
|
|
111
|
+
import json
|
|
112
|
+
output = {
|
|
113
|
+
"final_output": result.final_output,
|
|
114
|
+
"savings": result.savings.model_dump(),
|
|
115
|
+
"plan": {
|
|
116
|
+
"reasoning": result.plan.reasoning,
|
|
117
|
+
"subtasks": [t.model_dump() for t in result.plan.subtasks],
|
|
118
|
+
"estimated_total_tokens": result.plan.estimated_total_tokens,
|
|
119
|
+
},
|
|
120
|
+
"agent_results": [r.model_dump() for r in result.agent_results],
|
|
121
|
+
"total_duration_seconds": result.total_duration_seconds,
|
|
122
|
+
"recursion_depth": result.recursion_depth,
|
|
123
|
+
}
|
|
124
|
+
print(json.dumps(output, indent=2, default=str))
|
|
125
|
+
elif not args.quiet:
|
|
126
|
+
from tokensaver.utils import print_final_result
|
|
127
|
+
print_final_result(result)
|
|
128
|
+
else:
|
|
129
|
+
print(result.final_output)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def run_interactive(config: TokenSaverConfig) -> None:
|
|
133
|
+
"""Simple interactive session (great for exploration and demos)."""
|
|
134
|
+
console.rule("[bold cyan]TokenSaverAI Interactive Mode[/bold cyan]")
|
|
135
|
+
console.print("Type your complex task and press Enter. Type 'exit' or 'quit' to leave.\n")
|
|
136
|
+
|
|
137
|
+
mgr = Manager(config=config)
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
try:
|
|
141
|
+
prompt = Prompt.ask("[bold green]Task[/bold green]")
|
|
142
|
+
except (EOFError, KeyboardInterrupt):
|
|
143
|
+
console.print("\n[yellow]Goodbye.[/yellow]")
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
if prompt.lower().strip() in {"exit", "quit", "q"}:
|
|
147
|
+
break
|
|
148
|
+
if not prompt.strip():
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = await mgr.run(prompt)
|
|
153
|
+
from tokensaver.utils import print_final_result
|
|
154
|
+
print_final_result(result)
|
|
155
|
+
console.print("\n[dim]--- next task ---\n[/dim]")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def handle_wallet_commands(args: argparse.Namespace) -> None:
|
|
161
|
+
wallet = Wallet()
|
|
162
|
+
|
|
163
|
+
if args.command == "balance":
|
|
164
|
+
bal = wallet.get_balance()
|
|
165
|
+
console.print(f"\n[bold cyan]TokenSaverAI Prepay Wallet[/bold cyan]")
|
|
166
|
+
console.print(f" Prepaid Balance : [bold green]${bal.prepaid_balance:.2f}[/bold green]")
|
|
167
|
+
console.print(f" Total Spent : ${bal.total_spent:.2f}")
|
|
168
|
+
console.print(f" Total Credited : ${bal.total_credited:.2f}")
|
|
169
|
+
console.print(f" Last Updated : {bal.last_updated}")
|
|
170
|
+
|
|
171
|
+
if getattr(args, "detailed", False):
|
|
172
|
+
txs = wallet.get_recent_transactions(8)
|
|
173
|
+
if txs:
|
|
174
|
+
console.print("\n[bold]Recent Transactions:[/bold]")
|
|
175
|
+
for ts, typ, amt, src, desc in txs:
|
|
176
|
+
sign = "+" if typ == "credit" else "-"
|
|
177
|
+
console.print(f" {ts[:19]} {sign}${amt:.4f} ({src or desc or typ})")
|
|
178
|
+
|
|
179
|
+
elif args.command == "topup":
|
|
180
|
+
console.print("\n[bold cyan]How to Add Funds to Your TokenSaverAI Wallet[/bold cyan]\n")
|
|
181
|
+
console.print("1. Visit the payment link provided during onboarding (Stripe or Lemon Squeezy).")
|
|
182
|
+
console.print("2. After payment, run:")
|
|
183
|
+
console.print(" [bold]tokensaver add-funds 25[/bold] (local/manual credit)")
|
|
184
|
+
console.print(" or contact support for automatic crediting.")
|
|
185
|
+
console.print("\nThis prepay model guarantees you can never receive a surprise bill.")
|
|
186
|
+
|
|
187
|
+
elif args.command == "add-funds":
|
|
188
|
+
amount = args.amount
|
|
189
|
+
new_bal = wallet.credit(amount, source="cli")
|
|
190
|
+
console.print(f"[green]✓ Credited ${amount:.2f}[/green]")
|
|
191
|
+
console.print(f"New prepaid balance: [bold]${new_bal.prepaid_balance:.2f}[/bold]")
|
|
192
|
+
|
|
193
|
+
elif args.command == "config":
|
|
194
|
+
console.print("\n[bold]TokenSaverAI Configuration Wizard[/bold]")
|
|
195
|
+
console.print("This will help you set up Proxy Mode (recommended for production use).")
|
|
196
|
+
console.print("\nFor now, edit .tokensaver/config.toml and set proxy_mode = true when ready.")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def handle_proxy_commands(args: argparse.Namespace) -> None:
|
|
200
|
+
key_mgr = KeyManager()
|
|
201
|
+
cfg = load_config() # Load early for proxy commands (port, etc.)
|
|
202
|
+
|
|
203
|
+
if args.proxy_action == "start":
|
|
204
|
+
console.print("[bold cyan]Starting TokenSaverAI Proxy Server...[/bold cyan]\n")
|
|
205
|
+
console.print("Loading configuration from .env (including XAI_API_KEY)...\n")
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
import uvicorn
|
|
209
|
+
from tokensaver.proxy.server import app
|
|
210
|
+
|
|
211
|
+
port = getattr(cfg, "proxy_port", 8000)
|
|
212
|
+
console.print(f"[green]✓ Proxy starting on http://0.0.0.0:{port}[/green]")
|
|
213
|
+
console.print(f"[dim]Webhook URL: http://localhost:{port}/webhooks/lemonsqueezy[/dim]\n")
|
|
214
|
+
|
|
215
|
+
uvicorn.run(
|
|
216
|
+
"tokensaver.proxy.server:app",
|
|
217
|
+
host="0.0.0.0",
|
|
218
|
+
port=port,
|
|
219
|
+
reload=False,
|
|
220
|
+
log_level="info"
|
|
221
|
+
)
|
|
222
|
+
except ImportError:
|
|
223
|
+
console.print("[red]Proxy dependencies are missing.[/red]")
|
|
224
|
+
console.print("Install them with:")
|
|
225
|
+
console.print(" pip install -e \".[proxy]\"")
|
|
226
|
+
console.print("\nOr manually run:")
|
|
227
|
+
console.print(f" uvicorn tokensaver.proxy.server:app --port 8000")
|
|
228
|
+
except Exception as e:
|
|
229
|
+
console.print(f"[red]Failed to start proxy: {e}[/red]")
|
|
230
|
+
|
|
231
|
+
elif args.proxy_action == "create-key":
|
|
232
|
+
email = args.email
|
|
233
|
+
budget = args.budget
|
|
234
|
+
vk = key_mgr.create_key(email, budget)
|
|
235
|
+
|
|
236
|
+
console.print(f"[green]✓ Virtual key created for {email}[/green]")
|
|
237
|
+
console.print(f"Key: [bold]{vk.key}[/bold]")
|
|
238
|
+
console.print(f"Budget: ${budget:.2f}")
|
|
239
|
+
|
|
240
|
+
# Try to email it
|
|
241
|
+
try:
|
|
242
|
+
config = load_config()
|
|
243
|
+
email_cfg = getattr(config, "email", {}) or {}
|
|
244
|
+
sent = send_key_email(
|
|
245
|
+
to_email=email,
|
|
246
|
+
virtual_key=vk.key,
|
|
247
|
+
budget_usd=budget,
|
|
248
|
+
provider=email_cfg.get("provider", "smtp"),
|
|
249
|
+
smtp_host=email_cfg.get("smtp_host"),
|
|
250
|
+
smtp_user=email_cfg.get("smtp_user"),
|
|
251
|
+
smtp_password=email_cfg.get("smtp_password"),
|
|
252
|
+
from_email=email_cfg.get("from_email", "noreply@tokensaver.ai"),
|
|
253
|
+
resend_api_key=email_cfg.get("resend_api_key"),
|
|
254
|
+
)
|
|
255
|
+
if sent:
|
|
256
|
+
console.print("[green]✓ Key emailed to user[/green]")
|
|
257
|
+
else:
|
|
258
|
+
console.print("[yellow]Key created but email failed to send. Share it manually.[/yellow]")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
console.print(f"[yellow]Key created. Email sending failed: {e}[/yellow]")
|
|
261
|
+
|
|
262
|
+
elif args.proxy_action == "disable-key":
|
|
263
|
+
success = key_mgr.disable_key(args.key)
|
|
264
|
+
if success:
|
|
265
|
+
console.print(f"[green]✓ Key disabled: {args.key[:14]}...[/green]")
|
|
266
|
+
else:
|
|
267
|
+
console.print("[red]Key not found or already disabled[/red]")
|
|
268
|
+
|
|
269
|
+
elif args.proxy_action == "dashboard":
|
|
270
|
+
keys = key_mgr.list_keys(active_only=False)
|
|
271
|
+
console.print("\n[bold cyan]TokenSaverAI Proxy Dashboard[/bold cyan]\n")
|
|
272
|
+
console.print(f"Total Keys: {len(keys)}")
|
|
273
|
+
console.print(f"Active: {sum(1 for k in keys if k.is_active)}")
|
|
274
|
+
|
|
275
|
+
from tokensaver.wallet import Wallet
|
|
276
|
+
w = Wallet()
|
|
277
|
+
bal = w.get_balance()
|
|
278
|
+
console.print(f"Wallet Balance: [bold green]${bal.prepaid_balance:.2f}[/bold green]")
|
|
279
|
+
|
|
280
|
+
if keys:
|
|
281
|
+
console.print("\n[bold]Recent Keys:[/bold]")
|
|
282
|
+
for k in keys[:8]:
|
|
283
|
+
status = "ACTIVE" if k.is_active else "DISABLED"
|
|
284
|
+
rem = k.budget_usd - k.spent_usd
|
|
285
|
+
console.print(f" {k.user_email:<25} | ${rem:>6.2f} left | {status}")
|
|
286
|
+
|
|
287
|
+
admin_token = os.getenv("ADMIN_TOKEN")
|
|
288
|
+
if not admin_token:
|
|
289
|
+
console.print("\n[yellow]Warning: No ADMIN_TOKEN set. Admin routes are unprotected.[/yellow]")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def main() -> None:
|
|
293
|
+
parser = create_parser()
|
|
294
|
+
raw_args = sys.argv[1:]
|
|
295
|
+
|
|
296
|
+
known_commands = {"balance", "topup", "add-funds", "proxy", "config"}
|
|
297
|
+
|
|
298
|
+
# Find first non-flag token
|
|
299
|
+
first_pos = next((a for a in raw_args if not a.startswith("-")), None)
|
|
300
|
+
|
|
301
|
+
# Only attempt full subcommand parsing if the first token is a known command
|
|
302
|
+
if first_pos in known_commands:
|
|
303
|
+
args = parser.parse_args()
|
|
304
|
+
if args.init_config:
|
|
305
|
+
path = save_example_config()
|
|
306
|
+
console.print(f"[green]Example config written to {path}[/green]")
|
|
307
|
+
sys.exit(0)
|
|
308
|
+
|
|
309
|
+
if args.command in ("balance", "topup", "add-funds", "config"):
|
|
310
|
+
handle_wallet_commands(args)
|
|
311
|
+
return
|
|
312
|
+
if args.command == "proxy":
|
|
313
|
+
handle_proxy_commands(args)
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# === Task execution mode ===
|
|
317
|
+
# Use parse_known_args and be defensive in case it still complains
|
|
318
|
+
try:
|
|
319
|
+
args, unknown = parser.parse_known_args()
|
|
320
|
+
except SystemExit:
|
|
321
|
+
# Fallback: treat the entire command line as a task prompt
|
|
322
|
+
prompt_tokens = [a for a in raw_args if not a.startswith("-")]
|
|
323
|
+
prompt = " ".join(prompt_tokens).strip()
|
|
324
|
+
args = argparse.Namespace(
|
|
325
|
+
interactive=False,
|
|
326
|
+
plan_only="--plan-only" in raw_args,
|
|
327
|
+
powerful_model=None,
|
|
328
|
+
budget=None,
|
|
329
|
+
config=None,
|
|
330
|
+
quiet="--quiet" in raw_args,
|
|
331
|
+
json="--json" in raw_args,
|
|
332
|
+
init_config="--init-config" in raw_args,
|
|
333
|
+
)
|
|
334
|
+
unknown = []
|
|
335
|
+
else:
|
|
336
|
+
prompt = " ".join(unknown).strip()
|
|
337
|
+
|
|
338
|
+
if args.init_config:
|
|
339
|
+
path = save_example_config()
|
|
340
|
+
console.print(f"[green]Example config written to {path}[/green]")
|
|
341
|
+
sys.exit(0)
|
|
342
|
+
|
|
343
|
+
config = load_config(args.config)
|
|
344
|
+
if args.quiet:
|
|
345
|
+
config.verbose = False
|
|
346
|
+
|
|
347
|
+
if args.interactive or not prompt:
|
|
348
|
+
asyncio.run(run_interactive(config))
|
|
349
|
+
else:
|
|
350
|
+
asyncio.run(run_headless(prompt, args, config))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
main()
|