Added api key credential storage
This commit is contained in:
210
taletorrent/api_key.py
Normal file
210
taletorrent/api_key.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""Provider-agnostic API management using system keyring."""
|
||||
|
||||
import keyring
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt
|
||||
from rich.panel import Panel
|
||||
from openai import OpenAI
|
||||
import sys
|
||||
|
||||
console = Console()
|
||||
|
||||
# Service name for keyring
|
||||
SERVICE_NAME = "taletorrent"
|
||||
API_KEY_ITEM = "api_key"
|
||||
BASE_URL_ITEM = "base_url"
|
||||
|
||||
# Default configuration
|
||||
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
|
||||
|
||||
|
||||
class KeyringManager:
|
||||
"""Manages secure storage of API keys and base URLs using system keyring."""
|
||||
|
||||
@staticmethod
|
||||
def store_api_config(api_key: str, base_url: str) -> None:
|
||||
"""Store API key and base URL securely in system keyring."""
|
||||
try:
|
||||
keyring.set_password(SERVICE_NAME, API_KEY_ITEM, api_key)
|
||||
keyring.set_password(SERVICE_NAME, BASE_URL_ITEM, base_url)
|
||||
console.print(
|
||||
"✅ [green]API configuration stored securely in system keyring[/green]"
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]Error storing API configuration: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
@staticmethod
|
||||
def get_api_key() -> str:
|
||||
"""Retrieve API key from system keyring."""
|
||||
try:
|
||||
api_key = keyring.get_password(SERVICE_NAME, API_KEY_ITEM)
|
||||
if api_key is None:
|
||||
console.print("❌ [red]No API key found in system keyring[/red]")
|
||||
console.print(
|
||||
"Run [cyan]taletorrent config[/cyan] to set up your API configuration"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
return api_key
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]Error accessing keyring: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
@staticmethod
|
||||
def get_base_url() -> str:
|
||||
"""Retrieve base URL from system keyring."""
|
||||
try:
|
||||
base_url = keyring.get_password(SERVICE_NAME, BASE_URL_ITEM)
|
||||
if base_url is None:
|
||||
console.print("❌ [red]No base URL found in system keyring[/red]")
|
||||
console.print(
|
||||
"Run [cyan]taletorrent config[/cyan] to set up your API configuration"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
return base_url
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]Error accessing keyring: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
@staticmethod
|
||||
def has_api_config() -> bool:
|
||||
"""Check if API configuration exists in keyring."""
|
||||
try:
|
||||
api_key = keyring.get_password(SERVICE_NAME, API_KEY_ITEM)
|
||||
base_url = keyring.get_password(SERVICE_NAME, BASE_URL_ITEM)
|
||||
return api_key is not None and base_url is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def delete_api_config() -> None:
|
||||
"""Delete API configuration from system keyring."""
|
||||
try:
|
||||
keyring.delete_password(SERVICE_NAME, API_KEY_ITEM)
|
||||
keyring.delete_password(SERVICE_NAME, BASE_URL_ITEM)
|
||||
console.print(
|
||||
"✅ [green]API configuration removed from system keyring[/green]"
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]Error deleting API configuration: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def validate_api_config(api_key: str, base_url: str) -> bool:
|
||||
"""Validate API configuration by testing it against endpoint."""
|
||||
try:
|
||||
client = OpenAI(
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
# Test with a minimal request
|
||||
response = client.models.list()
|
||||
return True
|
||||
except Exception as e:
|
||||
console.print(f"❌ Connection failed: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def setup_api_config_wizard() -> None:
|
||||
"""Interactive wizard for setting up API configuration."""
|
||||
console.print(
|
||||
Panel.fit(
|
||||
"[bold cyan]API Configuration Setup[/bold cyan]\n\n"
|
||||
"Configure any OpenAI-compatible API endpoint.\n"
|
||||
"Your credentials will be stored securely in your system keyring.",
|
||||
title="API Provider Setup",
|
||||
)
|
||||
)
|
||||
|
||||
# Get base URL
|
||||
base_url = Prompt.ask("Enter API base URL", default=DEFAULT_BASE_URL)
|
||||
|
||||
# Get API key
|
||||
api_key = Prompt.ask("Enter API key", password=True)
|
||||
|
||||
while True:
|
||||
if not api_key.strip():
|
||||
console.print("❌ [red]API key cannot be empty[/red]")
|
||||
api_key = Prompt.ask("Enter API key", password=True)
|
||||
continue
|
||||
|
||||
console.print("🔄 [blue]Validating API configuration...[/blue]")
|
||||
|
||||
if validate_api_config(api_key, base_url):
|
||||
KeyringManager.store_api_config(api_key, base_url)
|
||||
console.print(
|
||||
"\n✅ [green]API configuration setup complete! You can now start playing.[/green]"
|
||||
)
|
||||
break
|
||||
else:
|
||||
console.print("⚠️ [yellow]Warning: API validation failed[/yellow]")
|
||||
console.print(
|
||||
"[dim]This may be due to network issues, invalid credentials, or backend compatibility issues.[/dim]"
|
||||
)
|
||||
|
||||
choice = Prompt.ask(
|
||||
"What would you like to do? [retry/force/cancel]",
|
||||
choices=["retry", "force", "cancel"],
|
||||
default="retry",
|
||||
)
|
||||
|
||||
if choice == "retry":
|
||||
base_url = Prompt.ask("Enter API base URL", default=base_url)
|
||||
api_key = Prompt.ask("Enter API key", password=True)
|
||||
continue
|
||||
elif choice == "force":
|
||||
console.print(
|
||||
"[yellow]⚠️ Storing configuration without validation[/yellow]"
|
||||
)
|
||||
console.print(
|
||||
"[dim]Note: You may encounter errors when using taletorrent if the configuration is invalid.[/dim]"
|
||||
)
|
||||
KeyringManager.store_api_config(api_key, base_url)
|
||||
console.print(
|
||||
"\n✅ [green]API configuration stored (validation skipped)[/green]"
|
||||
)
|
||||
break
|
||||
else: # cancel
|
||||
console.print("[yellow]Setup cancelled.[/yellow]")
|
||||
break
|
||||
|
||||
|
||||
def get_openai_client():
|
||||
"""Get configured OpenAI client with secure API credentials."""
|
||||
api_key = KeyringManager.get_api_key()
|
||||
base_url = KeyringManager.get_base_url()
|
||||
return OpenAI(
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
|
||||
def ensure_api_config() -> None:
|
||||
"""Ensure API configuration exists, prompt for setup if missing."""
|
||||
if not KeyringManager.has_api_config():
|
||||
console.print("❌ [red]No API configuration found[/red]")
|
||||
console.print(
|
||||
"Run [cyan]taletorrent config[/cyan] to set up your API configuration"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def get_api_info() -> dict:
|
||||
"""Get current API configuration info for display."""
|
||||
try:
|
||||
if not KeyringManager.has_api_config():
|
||||
return None
|
||||
|
||||
api_key = KeyringManager.get_api_key()
|
||||
base_url = KeyringManager.get_base_url()
|
||||
|
||||
return {
|
||||
"base_url": base_url,
|
||||
"api_key": api_key[:8] + "*" * (len(api_key) - 12) + api_key[-4:]
|
||||
if len(api_key) > 12
|
||||
else "*" * len(api_key),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
@@ -1,7 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from openai import OpenAI
|
||||
from .api_key import or_api_key
|
||||
|
||||
from .api_key import get_openai_client
|
||||
|
||||
# SETTINGS
|
||||
|
||||
@@ -71,10 +73,7 @@ Description of the caracter you play is provided below.
|
||||
creative_model = "deepseek/deepseek-r1-0528:free"
|
||||
pedantic_model = "qwen/qwen3-30b-a3b-instruct-2507"
|
||||
|
||||
openai_client = OpenAI(
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
api_key=or_api_key,
|
||||
)
|
||||
openai_client: OpenAI = get_openai_client()
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in New Issue
Block a user