feat(devpulse): API branch reinstate.
Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
@@ -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.
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user