feat(devpulse): API branch reinstate.

Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
AIOSAI
2026-04-10 00:39:01 -07:00
co-authored by @devpulse
parent 0d4541db06
commit 316345a130
67 changed files with 11091 additions and 458 deletions
+5
View File
@@ -0,0 +1,5 @@
# Modules
Business logic for `API`. One module per command.
Modules orchestrate work by calling handlers. They are the public API of the branch — drone routes commands here.
+233
View File
@@ -0,0 +1,233 @@
# =================== AIPass ====================
# Name: api_key.py
# Description: API Key Management Module
# Version: 1.0.0
# Created: 2025-11-15
# Modified: 2025-11-15
# =============================================
"""
API Key Management Module
Orchestrates API key and credential operations:
- Get/validate keys
- List providers
- Initialize .env template
"""
import sys
from pathlib import Path
from typing import List
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, header, success, error
from aipass.api.apps.handlers.json import json_handler
from aipass.api.apps.handlers.auth import keys, env
def print_introspection():
"""Show module introspection - connected handlers and capabilities"""
console.print()
header("API Key Module Introspection")
console.print()
console.print("[cyan]Purpose:[/cyan] API key management and validation")
console.print()
console.print("[cyan]Connected Handlers:[/cyan]")
console.print(" • api.apps.handlers.auth.keys")
console.print(" • api.apps.handlers.auth.env (template creation)")
console.print(" • api.apps.handlers.config.provider")
console.print(" • api.apps.handlers.json.json_handler")
console.print()
console.print("[cyan]Available Workflows:[/cyan]")
console.print(" • get_key() - Retrieve API key")
console.print(" • validate_key() - Validate credentials")
console.print(" • list_providers() - Show providers")
console.print(" • init_env() - Initialize configuration")
console.print()
def handle_command(command: str, args: List[str]) -> bool:
"""
Handle API key management commands
Args:
command: Command name
args: Command arguments
Returns:
True if command was handled, False otherwise
"""
try:
if command not in ["get-key", "validate", "list-providers", "init"]:
return False
# Help gate
if args and args[0] in ("--help", "-h", "help"):
print_help()
return True
# Log operation
json_handler.log_operation(f"api_key_{command}", {"command": command})
# Route all commands before introspection gate
if command == "list-providers":
list_providers()
return True
if command == "init":
init_env()
return True
if command == "get-key":
get_key(args)
return True
if command == "validate":
validate_key(args)
return True
# NO-ARGS GATE (seedgo standard) — only for unrecognized subcommands
if not args:
print_introspection()
return True
return True
except Exception as e:
logger.error(f"Error in api_key.handle_command: {e}")
raise
def get_key(args: List[str]):
"""Orchestrate key retrieval workflow"""
provider_name = args[0] if args else "openrouter"
header(f"Get API Key - {provider_name}")
console.print()
# Call handler to get key
api_key = keys.get_api_key(provider_name)
if api_key:
success(f"API key retrieved for {provider_name}")
masked = api_key[:6] + "****" + api_key[-4:] if len(api_key) > 10 else "****"
console.print(f" Key: {masked}")
else:
error(f"Failed to retrieve API key for {provider_name}")
def validate_key(args: List[str]):
"""Orchestrate key validation workflow"""
provider_name = args[0] if args else "openrouter"
header(f"Validate API Key - {provider_name}")
console.print()
# Get key from handler
api_key = keys.get_api_key(provider_name)
if not api_key:
error(f"No API key found for {provider_name}")
return
# Validate via handler
is_valid = keys.validate_key(api_key, provider_name)
if is_valid:
success(f"API key for {provider_name} is valid")
else:
error(f"API key for {provider_name} is invalid")
def list_providers():
"""Orchestrate list providers workflow"""
from aipass.api.apps.handlers.config.provider import PROVIDER_DEFAULTS
header("Available Providers")
console.print()
for provider_name in sorted(PROVIDER_DEFAULTS):
console.print(f" - {provider_name}")
console.print()
def init_env():
"""Orchestrate initialization workflow"""
header("Initialize API Configuration")
console.print()
env_path = Path.home() / ".secrets" / "aipass" / ".env"
if env_path.exists():
success(f"Environment file already exists at {env_path}")
return
# Create .env template via handler
if env.create_env_template():
success(f"Environment template created at {env_path}")
else:
error("Failed to create environment template")
def print_help():
"""Print help output for API key management"""
import argparse
parser = argparse.ArgumentParser(
prog="drone @api",
description='API Key Management Module - Manage API keys and credentials',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
COMMANDS:
get-key - Retrieve API key for a provider
validate - Validate API key
list-providers - List available providers
init - Initialize .env template
USAGE:
drone @api <command> [args]
drone @api --help
EXAMPLES:
# Get key for provider
drone @api get-key openrouter
# Validate key
drone @api validate openrouter
# List providers
drone @api list-providers
# Initialize environment
drone @api init
"""
)
console.print(parser.format_help())
if __name__ == "__main__":
"""Standalone execution mode"""
args = sys.argv[1:]
# Show introspection when run without arguments
if len(args) == 0:
print_introspection()
sys.exit(0)
# Show help for explicit help flags
if args[0] in ['--help', '-h', 'help']:
print_help()
sys.exit(0)
# Execute command
command = args[0]
remaining_args = args[1:] if len(args) > 1 else []
if handle_command(command, remaining_args):
sys.exit(0)
else:
console.print()
console.print(f"[red]Unknown command: {command}[/red]")
console.print()
console.print("Run [dim]drone @api --help[/dim] for available commands")
console.print()
sys.exit(1)
@@ -0,0 +1,364 @@
# =================== AIPass ====================
# Name: google_client.py
# Description: Google API Client Module — public API for Google services
# Version: 1.0.0
# Created: 2026-03-14
# Modified: 2026-03-14
# =============================================
"""
Google API Client Module
Public API for Google service access across AIPass.
Consumers import from here — never directly from handlers.
Provides:
- get_drive_service() → Authenticated Google Drive v3 client
- get_google_service() → Any Google API service (Calendar, Sheets, etc.)
- authenticate_google() → Run OAuth2 flow and return credentials
- validate_google() → Check if valid credentials exist
- reauth_google() → Force re-authentication
Consumer pattern:
from aipass.api.apps.modules.google_client import get_drive_service
service = get_drive_service()
service.files().list(...).execute()
Thread-safe pattern (for concurrent workers):
service = get_drive_service(thread_safe=True)
"""
import sys
from typing import List, Optional
from aipass.prax.apps.modules.logger import system_logger as logger # noqa: F811
from aipass.cli.apps.modules import console, header, success, error, warning
from aipass.api.apps.handlers.json import json_handler
import aipass.api.apps.handlers.google.auth as google_auth
import aipass.api.apps.handlers.google.service_factory as google_factory
import aipass.api.apps.handlers.google.retry as google_retry
# =============================================
# MODULE INTROSPECTION
# =============================================
def print_introspection() -> None:
"""Show module introspection — connected handlers and capabilities."""
console.print()
header("Google Client Module Introspection")
console.print()
console.print("[cyan]Purpose:[/cyan] Google API authentication and service factory")
console.print()
console.print("[cyan]Connected Handlers:[/cyan]")
console.print(" - api.apps.handlers.google.auth")
console.print(" - api.apps.handlers.google.service_factory")
console.print(" - api.apps.handlers.google.retry")
console.print()
console.print("[cyan]Available Workflows:[/cyan]")
console.print(" - get_drive_service() - Get authenticated Drive client")
console.print(" - get_google_service() - Get any Google API service")
console.print(" - authenticate_google() - Run OAuth2 authentication")
console.print(" - validate_google() - Check credential status")
console.print(" - reauth_google() - Force re-authentication")
console.print()
available = google_auth.is_available()
status = "[green]installed[/green]" if available else "[red]missing[/red]"
console.print(f"[cyan]Google Libraries:[/cyan] {status}")
has_creds = google_auth.CREDS_PATH.exists()
cred_status = "[green]found[/green]" if has_creds else "[yellow]not configured[/yellow]"
console.print(f"[cyan]Credentials:[/cyan] {cred_status}")
has_secret = google_auth.CLIENT_SECRET_PATH.exists()
secret_status = "[green]found[/green]" if has_secret else "[yellow]not configured[/yellow]"
console.print(f"[cyan]Client Secret:[/cyan] {secret_status}")
console.print()
def print_help() -> None:
"""Print module help."""
import argparse
parser = argparse.ArgumentParser(
prog="drone @api",
description="Google Client - Google API authentication and service access",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
COMMANDS (via drone @api):
validate google - Check Google OAuth2 credentials
reauth google - Re-run OAuth2 flow for Google
CROSS-BRANCH API:
from aipass.api.apps.modules.google_client import get_drive_service
service = get_drive_service()
CREDENTIAL SETUP:
1. Get OAuth client secret from Google Cloud Console
2. Save as: ~/.secrets/aipass/google_client_secret.json
3. Run: drone @api reauth google
4. Complete OAuth consent in browser
5. Credentials saved to: ~/.secrets/aipass/google_creds.json
"""
)
console.print(parser.format_help())
# =============================================
# COMMAND HANDLING (drone @api validate google, etc.)
# =============================================
def handle_command(command: str, args: List[str]) -> bool:
"""Handle Google client commands routed via drone.
Args:
command: Command name (e.g. "validate", "reauth")
args: Command arguments — first arg should be "google"
Returns:
True if command was handled, False to pass through.
"""
# NO-ARGS GATE (seedgo standard)
if not args:
if command == "google":
print_introspection()
return True
return False
# Only handle commands with "google" as the provider argument
if args[0] != "google":
return False
# Help gate — only fires after confirming this is a google command
remaining = args[1:] if len(args) > 1 else []
if remaining and remaining[0] in ("--help", "-h", "help"):
print_help()
return True
if command == "validate":
_cmd_validate()
return True
elif command == "reauth":
_cmd_reauth()
return True
return False
# =============================================
# CLI COMMAND IMPLEMENTATIONS
# =============================================
def _cmd_validate() -> None:
"""Validate Google OAuth2 credentials."""
header("Validate Google Credentials")
console.print()
if not google_auth.is_available():
error(
"Google auth libraries not installed",
suggestion="pip install google-auth google-auth-oauthlib google-api-python-client",
)
return
if not google_auth.CLIENT_SECRET_PATH.exists():
error(
"Client secret not found",
suggestion=f"Save OAuth client secret to: {google_auth.CLIENT_SECRET_PATH}",
)
return
if google_auth.validate_credentials():
success("Google credentials are valid")
json_handler.log_operation("google_validate", {"status": "valid"})
else:
warning("No valid Google credentials found")
console.print()
console.print("[dim]Run 'drone @api reauth google' to authenticate[/dim]")
json_handler.log_operation("google_validate", {"status": "invalid"})
def _cmd_reauth() -> None:
"""Force Google re-authentication via OAuth flow."""
header("Google Re-Authentication")
console.print()
if not google_auth.is_available():
error(
"Google auth libraries not installed",
suggestion="pip install google-auth google-auth-oauthlib google-api-python-client",
)
return
if not google_auth.CLIENT_SECRET_PATH.exists():
error(
"Client secret not found",
suggestion=f"Save OAuth client secret to: {google_auth.CLIENT_SECRET_PATH}",
)
return
warning("Starting OAuth2 flow...")
console.print("[dim]A browser window may open for Google consent.[/dim]")
console.print()
creds = google_auth.reauth()
if creds:
success("Google re-authentication successful")
console.print(f"[dim]Credentials saved to: {google_auth.CREDS_PATH}[/dim]")
json_handler.log_operation("google_reauth", {"status": "success"})
else:
error("Google re-authentication failed")
json_handler.log_operation("google_reauth", {"status": "failed"})
# =============================================
# PUBLIC API — Cross-branch imports
# =============================================
def get_drive_service(thread_safe: bool = False) -> object:
"""Get an authenticated Google Drive v3 service object.
Args:
thread_safe: If True, builds an isolated service instance
with fresh credentials from disk (for concurrent workers).
Returns:
Authenticated Drive v3 service object.
Raises:
RuntimeError: If authentication fails or libraries unavailable.
"""
return get_google_service("drive", "v3", thread_safe=thread_safe)
def get_google_service(
service_name: str = "drive",
version: str = "v3",
scopes: Optional[list] = None,
thread_safe: bool = False,
) -> object:
"""Get an authenticated Google API service object.
Supports any Google API: Drive, Calendar, Sheets, Gmail, etc.
Args:
service_name: Google API service (e.g. "drive", "calendar").
version: API version (e.g. "v3").
scopes: OAuth2 scopes. Uses service-specific defaults if not provided.
thread_safe: If True, builds an isolated instance for concurrent use.
Returns:
Authenticated service object.
Raises:
RuntimeError: If authentication fails or libraries unavailable.
"""
if not google_auth.is_available():
raise RuntimeError(
"Google auth libraries not installed. "
"Install: pip install google-auth google-auth-oauthlib google-api-python-client"
)
if thread_safe:
service = google_factory.build_thread_safe_service(
service_name, version, scopes
)
else:
service = google_factory.build_service(service_name, version, scopes)
if not service:
raise RuntimeError(
f"Failed to authenticate with Google {service_name} API. "
"Run 'drone @api reauth google' to set up credentials."
)
return service
def authenticate_google(scopes: Optional[list] = None) -> bool:
"""Run Google OAuth2 authentication.
Args:
scopes: OAuth2 scopes to request.
Returns:
True if authentication succeeded.
"""
creds = google_auth.authenticate(scopes=scopes)
return creds is not None
def validate_google(scopes: Optional[list] = None) -> bool:
"""Check if valid Google credentials exist.
Args:
scopes: OAuth2 scopes to validate against.
Returns:
True if valid credentials exist.
"""
return google_auth.validate_credentials(scopes=scopes)
def reauth_google(scopes: Optional[list] = None) -> bool:
"""Force Google re-authentication.
Args:
scopes: OAuth2 scopes to request.
Returns:
True if re-authentication succeeded.
"""
creds = google_auth.reauth(scopes=scopes)
return creds is not None
# Re-export retry utility for consumers that make raw API calls
def api_call_with_retry(*args, **kwargs):
"""Execute API call with retry logic for SSL and transient errors."""
return google_retry.api_call_with_retry(*args, **kwargs)
def is_ssl_error(error):
"""Check if an error is an SSL-related error."""
return google_retry.is_ssl_error(error)
# =============================================
# STANDALONE EXECUTION
# =============================================
if __name__ == "__main__":
args = sys.argv[1:]
if len(args) == 0:
print_introspection()
sys.exit(0)
if args[0] in ["--help", "-h", "help"]:
print_help()
sys.exit(0)
command = args[0]
remaining_args = args[1:] if len(args) > 1 else []
if handle_command(command, remaining_args):
sys.exit(0)
else:
console.print()
console.print(f"[red]Unknown command: {command}[/red]")
console.print()
console.print("Run [dim]drone @api --help[/dim] for available commands")
console.print()
sys.exit(1)
@@ -0,0 +1,380 @@
# =================== AIPass ====================
# Name: openrouter_client.py
# Description: OpenRouter Client Module
# Version: 1.0.0
# Created: 2025-11-15
# Modified: 2025-11-15
# =============================================
"""
OpenRouter Client Module
Orchestrates LLM API client operations:
- Test connections
- Make API calls
- List models
- Check status
"""
import sys
from typing import List
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, header, success, error
from aipass.api.apps.handlers.json import json_handler
from aipass.api.apps.handlers.auth import keys
from aipass.api.apps.handlers.openrouter import client, models
def print_introspection():
"""Show module introspection - connected handlers and capabilities"""
console.print()
header("OpenRouter Client Module Introspection")
console.print()
console.print("[cyan]Purpose:[/cyan] OpenRouter LLM API client operations")
console.print()
console.print("[cyan]Connected Handlers:[/cyan]")
console.print(" • api.apps.handlers.auth.keys")
console.print(" • api.apps.handlers.openrouter.client")
console.print(" • api.apps.handlers.openrouter.models")
console.print(" • api.apps.handlers.json.json_handler")
console.print()
console.print("[cyan]Available Workflows:[/cyan]")
console.print(" • test_connection() - Test connection")
console.print(" • make_call() - Make API call")
console.print(" • list_models() - List models")
console.print(" • check_status() - Check status")
console.print()
def print_help():
"""Print module help with argparse"""
import argparse
parser = argparse.ArgumentParser(
prog="drone @api",
description="OpenRouter Client - Manage LLM API connections",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
COMMANDS:
test - Test OpenRouter connection
call - Make API call to model
models - List available models
status - Check connection status
USAGE:
drone @api test
drone @api call <prompt> [--model MODEL]
drone @api models
drone @api status
ARGUMENTS:
prompt - Prompt to send to the model
--model - Model to use (optional)
EXAMPLES:
# Test OpenRouter connection
drone @api test
# Make an API call
drone @api call "What is AI?" --model gpt-4
# List available models
drone @api models
# Check connection status
drone @api status
"""
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# test command
subparsers.add_parser("test", help="Test OpenRouter connection")
# call command
call_parser = subparsers.add_parser("call", help="Make API call to model")
call_parser.add_argument("prompt", help="Prompt to send")
call_parser.add_argument("--model", help="Model to use")
# models command
subparsers.add_parser("models", help="List available models")
# status command
subparsers.add_parser("status", help="Check connection status")
console.print(parser.format_help())
def handle_command(command: str, args: List[str]) -> bool:
"""
Handle OpenRouter client commands
Args:
command: Command name
args: Command arguments
Returns:
True if command was handled, False otherwise
"""
try:
if command not in ["test", "call", "models", "status"]:
return False
# Help gate
if args and args[0] in ("--help", "-h", "help"):
print_help()
return True
# Log operation
json_handler.log_operation(f"openrouter_{command}", {"command": command})
# Route all commands before introspection gate
if command == "test":
test_connection()
return True
if command == "models":
list_models(args)
return True
if command == "status":
check_status()
return True
if command == "call":
make_call(args)
return True
# NO-ARGS GATE (seedgo standard) — only for unrecognized subcommands
if not args:
print_introspection()
return True
return True
except Exception as e:
logger.error(f"Error in openrouter_client.handle_command: {e}")
raise
def test_connection():
"""Orchestrate connection test workflow"""
header("Test OpenRouter Connection")
console.print()
console.print("[dim]Testing connection...[/dim]")
# Get API key via handler
api_key = keys.get_api_key("openrouter")
if not api_key:
diagnosis = keys.diagnose_key("openrouter")
error(diagnosis)
return
# Real API ping — hit /models endpoint
model_list = models.fetch_models_from_api(api_key)
if model_list:
success(f"Connection successful — {len(model_list)} models available")
else:
error("Connection failed — could not reach OpenRouter API")
def make_call(args: List[str]):
"""Orchestrate API call workflow"""
header("OpenRouter API Call")
console.print()
if not args:
error("Prompt required", suggestion="drone @api call \"your prompt\" --model MODEL")
return
# Parse args: first non-flag arg is prompt, --model MODEL is optional
prompt = None
model = None
i = 0
while i < len(args):
if args[i] == "--model" and i + 1 < len(args):
model = args[i + 1]
i += 2
elif prompt is None:
prompt = args[i]
i += 1
else:
i += 1
if not prompt:
error("Prompt required", suggestion="drone @api call \"your prompt\" --model MODEL")
return
if not model:
error("Model required", suggestion="drone @api call \"your prompt\" --model anthropic/claude-3.5-sonnet")
return
console.print(f"[dim]Calling {model}...[/dim]")
response = client.get_response(prompt, caller="cli", model=model)
if response:
success(f"Response received ({len(response['content'])} chars)")
console.print()
console.print(response["content"])
else:
error("API call failed")
def list_models(args: List[str] | None = None):
"""Orchestrate list models workflow"""
header("Available Models")
console.print()
show_all = args and "--all" in args
# Get API key via handler
api_key = keys.get_api_key("openrouter")
if not api_key:
diagnosis = keys.diagnose_key("openrouter")
error(diagnosis)
return
console.print("[dim]Fetching available models...[/dim]")
# Call handler to fetch models
model_list = models.fetch_models_from_api(api_key)
if not model_list:
error("Failed to fetch models")
return
success(f"Found {len(model_list)} models")
console.print()
# Format as table
display_count = len(model_list) if show_all else min(10, len(model_list))
console.print(f" {'Model':<50} {'Context':>10} {'$/prompt':>10} {'$/compl':>10}")
console.print(f" {'─' * 50} {'─' * 10} {'─' * 10} {'─' * 10}")
for model_data in model_list[:display_count]:
model_id = model_data.get("id", "unknown")
context = model_data.get("context_length", 0)
pricing = model_data.get("pricing", {})
prompt_cost = pricing.get("prompt", "0")
completion_cost = pricing.get("completion", "0")
# Format context length
if context >= 1_000_000:
ctx_str = f"{context // 1_000_000}M"
elif context >= 1_000:
ctx_str = f"{context // 1_000}k"
else:
ctx_str = str(context)
# Format pricing
if str(prompt_cost) == "0" and str(completion_cost) == "0":
p_str = "free"
c_str = "free"
else:
p_str = f"${prompt_cost}"
c_str = f"${completion_cost}"
console.print(f" {model_id:<50} {ctx_str:>10} {p_str:>10} {c_str:>10}")
if not show_all and len(model_list) > 10:
console.print()
console.print(f" [dim]Showing 10 of {len(model_list)} — use --all for full list[/dim]")
def check_status():
"""Orchestrate status check workflow"""
header("OpenRouter Client Status")
console.print()
# Key status
api_key = keys.get_api_key("openrouter")
if api_key:
masked = api_key[:8] + "..." + api_key[-4:]
console.print(f" [cyan]Key configured:[/cyan] [green]yes[/green]")
console.print(f" [cyan]Key:[/cyan] {masked}")
else:
console.print(f" [cyan]Key configured:[/cyan] [red]no[/red]")
diagnosis = keys.diagnose_key("openrouter")
console.print(f" [cyan]Reason:[/cyan] {diagnosis}")
console.print(f" [cyan]Provider:[/cyan] OpenRouter")
console.print(f" [cyan]Base URL:[/cyan] https://openrouter.ai/api/v1")
# OpenAI SDK availability
try:
import openai # noqa: F401
console.print(f" [cyan]OpenAI SDK:[/cyan] [green]available[/green]")
except ImportError:
logger.warning("OpenAI SDK not installed")
console.print(f" [cyan]OpenAI SDK:[/cyan] [red]missing[/red]")
# Client cache stats
cache_stats = client.get_cache_stats()
console.print(f" [cyan]Cached clients:[/cyan] {cache_stats['cached_clients']}/{cache_stats['max_cache_size']}")
console.print()
# =============================================
# PUBLIC API - Re-export handler functions
# =============================================
def get_response(prompt: str, caller: str | None = None, model: str | None = None, **kwargs):
"""
Public API: Get response from OpenRouter
This is a re-export of the handler function for cross-branch access.
Flow and other branches should use this module-level function instead
of importing directly from handlers.
Args:
prompt: User prompt text
caller: Module making the request (auto-detected if not provided)
model: Model to use (required - caller must provide from branch config)
**kwargs: Additional OpenAI API parameters
Returns:
Dict with 'content', 'id', 'model' or None on failure
Example:
>>> from aipass.api.apps.modules.openrouter_client import get_response
>>> response = get_response("Hello", caller="flow", model="anthropic/claude-3.5-sonnet")
>>> if response:
... console.print(response['content'])
"""
return client.get_response(prompt, caller, model, **kwargs)
if __name__ == "__main__":
"""Standalone execution mode"""
args = sys.argv[1:]
# Show introspection when run without arguments
if len(args) == 0:
print_introspection()
sys.exit(0)
# Show help for explicit help flags
if args[0] in ['--help', '-h', 'help']:
print_help()
sys.exit(0)
# Execute command
command = args[0]
remaining_args = args[1:] if len(args) > 1 else []
if handle_command(command, remaining_args):
sys.exit(0)
else:
console.print()
console.print(f"[red]Unknown command: {command}[/red]")
console.print()
console.print("Run [dim]drone @api --help[/dim] for available commands")
console.print()
sys.exit(1)
@@ -0,0 +1,301 @@
# =================== AIPass ====================
# Name: usage_tracker.py
# Description: Usage Tracking Module
# Version: 1.0.0
# Created: 2025-11-15
# Modified: 2025-11-15
# =============================================
"""
Usage Tracking Module
Orchestrates API usage monitoring operations:
- Track generation usage
- Display statistics
- Session summaries
- Cleanup old data
"""
import sys
from pathlib import Path
from typing import List
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, header, success, error, warning
from aipass.api.apps.handlers.json import json_handler
from aipass.api.apps.handlers.usage import tracking, aggregation, cleanup
from aipass.api.apps.handlers.usage.cleanup import DEFAULT_RETENTION_DAYS
def print_introspection():
"""Show module introspection - connected handlers and capabilities"""
console.print()
header("Usage Tracker Module Introspection")
console.print()
console.print("[cyan]Purpose:[/cyan] API usage monitoring and cost tracking")
console.print()
console.print("[cyan]Connected Handlers:[/cyan]")
console.print(" • api.apps.handlers.usage.tracking")
console.print(" • api.apps.handlers.usage.aggregation")
console.print(" • api.apps.handlers.usage.cleanup")
console.print(" • api.apps.handlers.json.json_handler")
console.print()
console.print("[cyan]Available Workflows:[/cyan]")
console.print(" • track_usage() - Track usage")
console.print(" • show_stats() - Show statistics")
console.print(" • show_session() - Show session")
console.print(" • show_caller_usage() - Caller stats")
console.print(" • cleanup_data() - Clean old data")
console.print()
def print_help():
"""Print module help with argparse"""
import argparse
parser = argparse.ArgumentParser(
prog="drone @api",
description="Usage Tracker - Monitor API usage and costs",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
COMMANDS:
track - Track API usage
stats - Show usage statistics
session - Show session data
caller-usage - Show usage by caller
cleanup - Clean up old usage data
USAGE:
drone @api track <caller>
drone @api stats
drone @api session
drone @api caller-usage <caller>
drone @api cleanup [days]
ARGUMENTS:
caller - Caller identifier
days - Number of days to retain (default: 30)
EXAMPLES:
# Track usage for a caller
drone @api track my_application
# Show usage statistics
drone @api stats
# Show session data
drone @api session
# Show usage for specific caller
drone @api caller-usage my_application
# Cleanup data older than 60 days
drone @api cleanup 60
"""
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# track command
track_parser = subparsers.add_parser("track", help="Track API usage")
track_parser.add_argument("caller", help="Caller identifier")
# stats command
subparsers.add_parser("stats", help="Show usage statistics")
# session command
subparsers.add_parser("session", help="Show session data")
# caller-usage command
caller_parser = subparsers.add_parser("caller-usage", help="Show usage by caller")
caller_parser.add_argument("caller", help="Caller identifier")
# cleanup command
cleanup_parser = subparsers.add_parser("cleanup", help="Clean up old usage data")
cleanup_parser.add_argument("days", nargs="?", default=str(DEFAULT_RETENTION_DAYS), help=f"Days to retain (default: {DEFAULT_RETENTION_DAYS})")
console.print(parser.format_help())
def handle_command(command: str, args: List[str]) -> bool:
"""
Handle usage tracking commands
Args:
command: Command name
args: Command arguments
Returns:
True if command was handled, False otherwise
"""
try:
if command not in ["track", "stats", "session", "caller-usage", "cleanup"]:
return False
# Help gate
if args and args[0] in ("--help", "-h", "help"):
print_help()
return True
# Log operation
json_handler.log_operation(f"usage_{command}", {"command": command})
# Route all commands before introspection gate
if command == "stats":
show_stats()
return True
if command == "session":
show_session()
return True
if command == "track":
track_usage(args)
return True
if command == "caller-usage":
show_caller_usage(args)
return True
if command == "cleanup":
cleanup_data(args)
return True
# NO-ARGS GATE (seedgo standard) — only for unrecognized subcommands
if not args:
print_introspection()
return True
return True
except Exception as e:
logger.error(f"Error in usage_tracker.handle_command: {e}")
raise
def track_usage(args: List[str]):
"""Orchestrate usage tracking workflow"""
header("Track API Usage")
console.print()
if not args:
error("Generation ID required", suggestion="drone @api track <generation_id> [caller]")
return
generation_id = args[0]
caller = args[1] if len(args) > 1 else "manual"
console.print(f"[dim]Tracking generation {generation_id}...[/dim]")
result = tracking.track_usage(generation_id, caller)
if result.get("success"):
metrics = result.get("metrics", {})
success(f"Tracked: {metrics.get('tokens_prompt', 0)} prompt + {metrics.get('tokens_completion', 0)} completion tokens, ${metrics.get('total_cost', 0):.6f}")
else:
error(f"Tracking failed: {result.get('error', 'unknown')}")
def show_stats():
"""Orchestrate statistics display workflow"""
header("Usage Statistics")
console.print()
# Call handler for session summary
stats = aggregation.get_session_summary()
if stats:
console.print(f" Total Requests: {stats.get('total_requests', 0)}")
console.print(f" Total Cost: ${stats.get('total_cost', 0.0):.6f}")
console.print(f" Total Tokens: {stats.get('total_tokens', 0)}")
else:
warning("No usage data available")
def show_session():
"""Orchestrate session summary workflow"""
header("Session Summary")
console.print()
# Call handler for session data
summary = aggregation.get_session_summary()
if summary:
console.print(f" Session Requests: {summary.get('total_requests', 0)}")
console.print(f" Session Cost: ${summary.get('total_cost', 0.0):.6f}")
console.print(f" Session Tokens: {summary.get('total_tokens', 0)}")
else:
warning("No session data available")
def show_caller_usage(args: List[str]):
"""Orchestrate caller usage display workflow"""
if not args:
error("Caller name required")
return
caller = args[0]
header(f"Usage for Caller: {caller}")
console.print()
# Call handler for caller stats
usage = aggregation.get_caller_usage(caller)
if usage:
console.print(f" Requests: {usage.get('requests', 0)}")
console.print(f" Total Cost: ${usage.get('total_cost', 0.0):.6f}")
console.print(f" Total Tokens: {usage.get('total_tokens', 0)}")
else:
warning(f"No usage data found for caller: {caller}")
def cleanup_data(args: List[str]):
"""Orchestrate cleanup workflow"""
days = int(args[0]) if args else DEFAULT_RETENTION_DAYS
header(f"Cleanup Old Data (retain {days} days)")
console.print()
# Call handler for cleanup
# Navigate: usage_tracker.py -> modules/ -> apps/ -> api/
API_JSON_DIR = Path(__file__).resolve().parent.parent.parent / "api_json"
data_path = API_JSON_DIR / "usage_tracker_data.json"
if cleanup.cleanup_old_data(data_path, days):
success(f"Cleaned up data older than {days} days")
# Fire trigger event
try:
from aipass.trigger.apps.modules.core import trigger
trigger.fire('usage_data_cleaned', days=days, data_path=str(data_path))
except ImportError:
logger.warning("Trigger module not available — skipping event fire")
else:
error("Cleanup failed")
if __name__ == "__main__":
"""Standalone execution mode"""
args = sys.argv[1:]
# Show introspection when run without arguments
if len(args) == 0:
print_introspection()
sys.exit(0)
# Show help for explicit help flags
if args[0] in ['--help', '-h', 'help']:
print_help()
sys.exit(0)
# Execute command
command = args[0]
remaining_args = args[1:] if len(args) > 1 else []
if handle_command(command, remaining_args):
sys.exit(0)
else:
console.print()
console.print(f"[red]Unknown command: {command}[/red]")
console.print()
console.print("Run [dim]drone @api --help[/dim] for available commands")
console.print()
sys.exit(1)