feat(system): README update, API init fix, diagnostic docs, STATUS sync
- README.md: updated to current state (15 branches, 95+ PRs, 733 tests, 173 commands) - API: init path fixed (creates .env at ~/.secrets/aipass/ not src/aipass/api/) - API: init shows path and detects existing .env - devpulse/docs: system_health_report, testing_standards, ai_mail comms upgrade - STATUS.md: synced via drone @prax status sync Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a800e7919a
commit
ec63b8ba29
@@ -152,17 +152,17 @@ def create_env_template(provider: str = "openrouter", target_path: Optional[Path
|
||||
|
||||
Args:
|
||||
provider: API provider name (default: 'openrouter')
|
||||
target_path: Optional custom path (defaults to api/.env)
|
||||
target_path: Optional custom path (defaults to ~/.secrets/aipass/.env)
|
||||
|
||||
Returns:
|
||||
bool: True if template created successfully, False otherwise
|
||||
|
||||
Example:
|
||||
>>> if create_env_template('openrouter'):
|
||||
... print("Template created at <api_root>/.env")
|
||||
... print("Template created at ~/.secrets/aipass/.env")
|
||||
"""
|
||||
# Default to api/.env
|
||||
env_path = target_path or (API_ROOT / ".env")
|
||||
# Default to ~/.secrets/aipass/.env (cross-platform standard)
|
||||
env_path = target_path or (Path.home() / ".secrets" / "aipass" / ".env")
|
||||
|
||||
# Don't overwrite existing file
|
||||
if env_path.exists():
|
||||
|
||||
@@ -268,6 +268,53 @@ def get_validation_rules(provider: str) -> Dict[str, Any]:
|
||||
# KEY FORMAT CHECKING
|
||||
# ==============================================
|
||||
|
||||
def diagnose_key(provider: str = "openrouter") -> str:
|
||||
"""
|
||||
Diagnose why get_api_key() returned None.
|
||||
|
||||
Checks all sources for a raw key (skipping validation) and explains
|
||||
exactly why it failed — missing entirely, wrong prefix, too short, etc.
|
||||
|
||||
Args:
|
||||
provider: Provider name (default: 'openrouter')
|
||||
|
||||
Returns:
|
||||
str: Human-readable explanation of the key issue
|
||||
|
||||
Example:
|
||||
>>> if not get_api_key('openrouter'):
|
||||
... print(diagnose_key('openrouter'))
|
||||
"""
|
||||
# Check all sources for raw key (without validation)
|
||||
key = get_key_from_config(provider)
|
||||
source = "config"
|
||||
|
||||
if not key:
|
||||
key = get_key_from_env(provider)
|
||||
source = "env"
|
||||
|
||||
if not key:
|
||||
env_var = f"{provider.upper()}_API_KEY"
|
||||
key = read_env_file(env_var)
|
||||
source = "dotenv"
|
||||
|
||||
if not key:
|
||||
return "No API key found in any source (config, environment, .env file)"
|
||||
|
||||
# Key exists but failed validation — explain why
|
||||
key = key.strip()
|
||||
rules = get_validation_rules(provider)
|
||||
|
||||
if "prefix" in rules and not key.startswith(rules["prefix"]):
|
||||
actual_prefix = key[:len(rules["prefix"])] if len(key) >= len(rules["prefix"]) else key[:6]
|
||||
return f"Key found ({source}) but invalid — expected prefix '{rules['prefix']}', got '{actual_prefix}...'"
|
||||
|
||||
if "min_length" in rules and len(key) < rules["min_length"]:
|
||||
return f"Key found ({source}) but too short — {len(key)} chars, need {rules['min_length']}+"
|
||||
|
||||
return f"Key found ({source}) but failed validation"
|
||||
|
||||
|
||||
def check_key_format(key: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze key format and return details.
|
||||
|
||||
@@ -56,6 +56,7 @@ except ImportError:
|
||||
# Handler imports
|
||||
from aipass.api.apps.handlers.auth.keys import get_api_key
|
||||
from aipass.api.apps.handlers.openrouter.caller import get_caller_info
|
||||
from aipass.api.apps.handlers.openrouter.provision import ensure_caller_config
|
||||
from aipass.api.apps.handlers.usage.tracking import track_usage
|
||||
|
||||
# JSON handler
|
||||
@@ -315,6 +316,12 @@ def get_response(prompt: str, caller: Optional[str] = None, model: Optional[str]
|
||||
# logger.warning("Could not detect caller - using 'unknown'")
|
||||
caller = "unknown"
|
||||
|
||||
# Step 1b: Ensure caller has config (auto-provision if missing)
|
||||
try:
|
||||
ensure_caller_config(caller)
|
||||
except Exception:
|
||||
pass # Provisioning is best-effort — don't block the API call
|
||||
|
||||
# Step 2: Require model from caller - no defaults
|
||||
if not model:
|
||||
# logger.error("No model specified - caller must provide model from their branch config")
|
||||
|
||||
@@ -153,9 +153,15 @@ def init_env():
|
||||
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("Environment template created")
|
||||
success(f"Environment template created at {env_path}")
|
||||
else:
|
||||
error("Failed to create environment template")
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
test_connection()
|
||||
return True
|
||||
if command == "models":
|
||||
list_models()
|
||||
list_models(args)
|
||||
return True
|
||||
if command == "status":
|
||||
check_status()
|
||||
@@ -164,17 +164,23 @@ def test_connection():
|
||||
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:
|
||||
error("No API key configured")
|
||||
diagnosis = keys.diagnose_key("openrouter")
|
||||
error(diagnosis)
|
||||
return
|
||||
|
||||
warning("Testing connection...")
|
||||
# Real API ping — hit /models endpoint
|
||||
model_list = models.fetch_models_from_api(api_key)
|
||||
|
||||
# TODO: Call handler to test connection when implemented
|
||||
success("Connection test successful")
|
||||
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]):
|
||||
@@ -187,31 +193,67 @@ def make_call(args: List[str]):
|
||||
warning("API call workflow - TODO")
|
||||
|
||||
|
||||
def list_models():
|
||||
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:
|
||||
error("No API key configured")
|
||||
diagnosis = keys.diagnose_key("openrouter")
|
||||
error(diagnosis)
|
||||
return
|
||||
|
||||
warning("Fetching available models...")
|
||||
console.print("[dim]Fetching available models...[/dim]")
|
||||
|
||||
# Call handler to fetch models
|
||||
model_list = models.fetch_models_from_api(api_key)
|
||||
|
||||
if model_list:
|
||||
success(f"Found {len(model_list)} models")
|
||||
for model in model_list[:10]:
|
||||
console.print(f" - {model}")
|
||||
if len(model_list) > 10:
|
||||
console.print(f" ... and {len(model_list) - 10} more")
|
||||
else:
|
||||
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():
|
||||
@@ -219,8 +261,32 @@ def check_status():
|
||||
header("OpenRouter Client Status")
|
||||
console.print()
|
||||
|
||||
# TODO: Call handlers for status when implemented
|
||||
warning("Client status - TODO")
|
||||
# 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:
|
||||
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()
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
Reference in New Issue
Block a user