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 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()