1.9.6 #1

Merged
rune merged 3 commits from 1.9.6 into main 2025-12-30 15:46:41 +01:00
Showing only changes of commit bea9c86fa2 - Show all commits

142
oai.py
View File

@@ -30,7 +30,7 @@ from packaging import version as pkg_version
import io # Added for custom handler import io # Added for custom handler
# App version. Changes by author with new releases. # App version. Changes by author with new releases.
version = '1.9.5' version = '1.9.6'
app = typer.Typer() app = typer.Typer()
@@ -369,41 +369,132 @@ class RotatingRichHandler(RotatingFileHandler):
except Exception: except Exception:
self.handleError(record) self.handleError(record)
# Get log configuration from DB # ============================================================================
# LOGGING SETUP - MUST BE DONE AFTER CONFIG IS LOADED
# ============================================================================
# Load log configuration from DB FIRST (before creating handler)
LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10") LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10")
LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2") LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2")
LOG_LEVEL_STR = get_config('log_level') or "info" LOG_LEVEL_STR = get_config('log_level') or "info"
LOG_LEVEL = VALID_LOG_LEVELS.get(LOG_LEVEL_STR.lower(), logging.INFO) LOG_LEVEL = VALID_LOG_LEVELS.get(LOG_LEVEL_STR.lower(), logging.INFO)
# Create the custom rotating handler # Global reference to the handler for dynamic reloading
app_handler = RotatingRichHandler( app_handler = None
app_logger = None
def setup_logging():
"""Setup or reset logging configuration with current settings."""
global app_handler, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, app_logger
# Get the root logger
root_logger = logging.getLogger()
# Remove existing handler if present
if app_handler is not None:
root_logger.removeHandler(app_handler)
try:
app_handler.close()
except:
pass
# Check if log file needs immediate rotation
if os.path.exists(log_file):
current_size = os.path.getsize(log_file)
max_bytes = LOG_MAX_SIZE_MB * 1024 * 1024
if current_size >= max_bytes:
# Perform immediate rotation
import shutil
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file = f"{log_file}.{timestamp}"
try:
shutil.move(str(log_file), backup_file)
except Exception as e:
print(f"Warning: Could not rotate log file: {e}")
# Clean up old backups if exceeding limit
log_dir = os.path.dirname(log_file)
log_basename = os.path.basename(log_file)
backup_pattern = f"{log_basename}.*"
import glob
backups = sorted(glob.glob(os.path.join(log_dir, backup_pattern)))
# Keep only the most recent backups
while len(backups) > LOG_BACKUP_COUNT:
oldest = backups.pop(0)
try:
os.remove(oldest)
except:
pass
# Create new handler with current settings
app_handler = RotatingRichHandler(
filename=str(log_file), filename=str(log_file),
maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024, # Convert MB to bytes maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024,
backupCount=LOG_BACKUP_COUNT, backupCount=LOG_BACKUP_COUNT,
encoding='utf-8' encoding='utf-8'
) )
logging.basicConfig( # Set handler level to NOTSET so it processes all records
level=logging.NOTSET, app_handler.setLevel(logging.NOTSET)
format="%(message)s", # Rich formats it
datefmt="[%X]",
handlers=[app_handler]
)
app_logger = logging.getLogger("oai_app") # Configure root logger - set to WARNING to suppress third-party library noise
app_logger.setLevel(LOG_LEVEL) root_logger.setLevel(logging.WARNING)
root_logger.addHandler(app_handler)
# Suppress noisy third-party loggers
# These libraries create DEBUG logs that pollute our log file
logging.getLogger('asyncio').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('httpcore').setLevel(logging.WARNING)
logging.getLogger('openai').setLevel(logging.WARNING)
logging.getLogger('openrouter').setLevel(logging.WARNING)
# Get or create app logger and set its level (this filters what gets logged)
app_logger = logging.getLogger("oai_app")
app_logger.setLevel(LOG_LEVEL)
# Don't propagate to avoid root logger filtering
app_logger.propagate = True
return app_logger
# Initial logging setup
app_logger = setup_logging()
def set_log_level(level_str: str) -> bool: def set_log_level(level_str: str) -> bool:
"""Set the application log level. Returns True if successful.""" """Set the application log level. Returns True if successful."""
global LOG_LEVEL, LOG_LEVEL_STR global LOG_LEVEL, LOG_LEVEL_STR, app_logger
level_str_lower = level_str.lower() level_str_lower = level_str.lower()
if level_str_lower not in VALID_LOG_LEVELS: if level_str_lower not in VALID_LOG_LEVELS:
return False return False
LOG_LEVEL = VALID_LOG_LEVELS[level_str_lower] LOG_LEVEL = VALID_LOG_LEVELS[level_str_lower]
LOG_LEVEL_STR = level_str_lower LOG_LEVEL_STR = level_str_lower
# Update the logger level immediately
if app_logger:
app_logger.setLevel(LOG_LEVEL) app_logger.setLevel(LOG_LEVEL)
return True return True
def reload_logging_config():
"""Reload logging configuration from database and reinitialize handler."""
global LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR, app_logger
# Reload from database
LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10")
LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2")
LOG_LEVEL_STR = get_config('log_level') or "info"
LOG_LEVEL = VALID_LOG_LEVELS.get(LOG_LEVEL_STR.lower(), logging.INFO)
# Reinitialize logging
app_logger = setup_logging()
return app_logger
# ============================================================================ # ============================================================================
# END OF LOGGING SETUP # END OF LOGGING SETUP
# ============================================================================ # ============================================================================
@@ -699,7 +790,7 @@ def show_command_help(command: str):
app_logger.info(f"Displayed detailed help for command: {command}") app_logger.info(f"Displayed detailed help for command: {command}")
# Load configs # Load configs (AFTER logging is set up)
API_KEY = get_config('api_key') API_KEY = get_config('api_key')
OPENROUTER_BASE_URL = get_config('base_url') or "https://openrouter.ai/api/v1" OPENROUTER_BASE_URL = get_config('base_url') or "https://openrouter.ai/api/v1"
STREAM_ENABLED = get_config('stream_enabled') or "on" STREAM_ENABLED = get_config('stream_enabled') or "on"
@@ -909,7 +1000,7 @@ def display_paginated_table(table: Table, title: str):
@app.command() @app.command()
def chat(): def chat():
global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR, app_logger
session_max_token = 0 session_max_token = 0
session_system_prompt = "" session_system_prompt = ""
session_history = [] session_history = []
@@ -1319,10 +1410,14 @@ def chat():
console.print("[bold yellow]Usage: /middleout on|off (or /middleout to view status)[/]") console.print("[bold yellow]Usage: /middleout on|off (or /middleout to view status)[/]")
continue continue
elif user_input.lower() == "/reset": elif user_input.lower() == "/reset":
try:
confirm = typer.confirm("Reset conversation context? This clears history and prompt.", default=False) confirm = typer.confirm("Reset conversation context? This clears history and prompt.", default=False)
if not confirm: if not confirm:
console.print("[bold yellow]Reset cancelled.[/]") console.print("[bold yellow]Reset cancelled.[/]")
continue continue
except (EOFError, KeyboardInterrupt):
console.print("\n[bold yellow]Reset cancelled.[/]")
continue
session_history = [] session_history = []
current_index = -1 current_index = -1
session_system_prompt = "" session_system_prompt = ""
@@ -1538,9 +1633,13 @@ def chat():
new_size_mb = 100 new_size_mb = 100
set_config('log_max_size_mb', str(new_size_mb)) set_config('log_max_size_mb', str(new_size_mb))
LOG_MAX_SIZE_MB = new_size_mb LOG_MAX_SIZE_MB = new_size_mb
console.print(f"[bold green]Log size limit set to {new_size_mb} MB.[/]")
console.print("[bold yellow]⚠️ Restart the application for this change to take effect.[/]") # Reload logging configuration immediately
app_logger.info(f"Log size limit updated to {new_size_mb} MB (requires restart)") app_logger = reload_logging_config()
console.print(f"[bold green]Log size limit set to {new_size_mb} MB and applied immediately.[/]")
console.print(f"[dim cyan]Log file rotated if it exceeded the new limit.[/]")
app_logger.info(f"Log size limit updated to {new_size_mb} MB and reloaded")
except ValueError: except ValueError:
console.print("[bold red]Invalid size. Provide a number in MB.[/]") console.print("[bold red]Invalid size. Provide a number in MB.[/]")
elif args.startswith("online"): elif args.startswith("online"):
@@ -1778,7 +1877,7 @@ def chat():
) )
help_table.add_row( help_table.add_row(
"/config log [size_mb]", "/config log [size_mb]",
"Set log file size limit in MB. Older logs are rotated automatically. Requires restart.", "Set log file size limit in MB. Older logs are rotated automatically. Takes effect immediately.",
"/config log 20" "/config log 20"
) )
help_table.add_row( help_table.add_row(
@@ -1930,7 +2029,6 @@ def chat():
if not selected_model: if not selected_model:
console.print("[bold yellow]Select a model first with '/model'.[/]") console.print("[bold yellow]Select a model first with '/model'.[/]")
continue continue
# Process file attachments with PDF support # Process file attachments with PDF support
content_blocks = [] content_blocks = []
text_part = user_input text_part = user_input