1 Commits
1.9 ... main

Author SHA1 Message Date
a6f0edd9f3 New functionality++ Verision bump 2025-12-23 15:08:20 +01:00
3 changed files with 235 additions and 116 deletions

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@ build*
compiled/ compiled/
images/oai-iOS-Default-1024x1024@1x.png images/oai-iOS-Default-1024x1024@1x.png
images/oai.icon/ images/oai.icon/
b0.sh

339
oai.py
View File

@@ -20,11 +20,17 @@ import re
import sqlite3 import sqlite3
import json import json
import datetime import datetime
import logging # Added missing import for logging import logging
from logging.handlers import RotatingFileHandler # Added for log rotation
from prompt_toolkit import PromptSession from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory from prompt_toolkit.history import FileHistory
from rich.logging import RichHandler from rich.logging import RichHandler
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from packaging import version as pkg_version
import io # Added for custom handler
# App version. Changes by author with new releases.
version = '1.9.5'
app = typer.Typer() app = typer.Typer()
@@ -71,27 +77,9 @@ LOW_CREDIT_RATIO = 0.1 # Warn if credits left < 10% of total
LOW_CREDIT_AMOUNT = 1.0 # Warn if credits left < $1 in absolute terms LOW_CREDIT_AMOUNT = 1.0 # Warn if credits left < $1 in absolute terms
HIGH_COST_WARNING = "cost_warning_threshold" # Configurable key for cost threshold, default $0.01 HIGH_COST_WARNING = "cost_warning_threshold" # Configurable key for cost threshold, default $0.01
# Setup Rich-powered logging to file as per [rich.readthedocs.io](https://rich.readthedocs.io/en/latest/logging.html)
log_console = Console(file=open(log_file, 'a'), width=120) # Append to log file, wider for tracebacks
handler = RichHandler(
console=log_console,
level=logging.INFO,
rich_tracebacks=True,
tracebacks_suppress=['requests', 'openrouter', 'urllib3', 'httpx', 'openai'] # Suppress irrelevant tracebacks for cleaner logs
)
logging.basicConfig(
level=logging.NOTSET, # Let handler control what gets written
format="%(message)s", # Rich formats it
datefmt="[%X]",
handlers=[handler]
)
app_logger = logging.getLogger("oai_app")
app_logger.setLevel(logging.INFO)
# DB configuration # DB configuration
database = config_dir / 'oai_config.db' database = config_dir / 'oai_config.db'
DB_FILE = str(database) DB_FILE = str(database)
version = '1.9'
def create_table_if_not_exists(): def create_table_if_not_exists():
"""Ensure the config and conversation_sessions tables exist.""" """Ensure the config and conversation_sessions tables exist."""
@@ -122,6 +110,124 @@ def set_config(key: str, value: str):
conn.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', (key, value)) conn.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', (key, value))
conn.commit() conn.commit()
# ============================================================================
# ROTATING RICH HANDLER - Combines RotatingFileHandler with Rich formatting
# ============================================================================
class RotatingRichHandler(RotatingFileHandler):
"""Custom handler that combines RotatingFileHandler with Rich formatting."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Create a Rich console that writes to a string buffer
self.rich_console = Console(file=io.StringIO(), width=120, force_terminal=False)
self.rich_handler = RichHandler(
console=self.rich_console,
show_time=True,
show_path=True,
rich_tracebacks=True,
tracebacks_suppress=['requests', 'openrouter', 'urllib3', 'httpx', 'openai']
)
def emit(self, record):
try:
# Let RichHandler format the record
self.rich_handler.emit(record)
# Get the formatted output from the string buffer
output = self.rich_console.file.getvalue()
# Clear the buffer for next use
self.rich_console.file.seek(0)
self.rich_console.file.truncate(0)
# Write the Rich-formatted output to our rotating file
if output:
self.stream.write(output)
self.flush()
except Exception:
self.handleError(record)
# Get log configuration from DB
LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10")
LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2")
# Create the custom rotating handler
app_handler = RotatingRichHandler(
filename=str(log_file),
maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024, # Convert MB to bytes
backupCount=LOG_BACKUP_COUNT,
encoding='utf-8'
)
logging.basicConfig(
level=logging.NOTSET,
format="%(message)s", # Rich formats it
datefmt="[%X]",
handlers=[app_handler]
)
app_logger = logging.getLogger("oai_app")
app_logger.setLevel(logging.INFO)
# ============================================================================
# END OF LOGGING SETUP
# ============================================================================
logger = logging.getLogger(__name__)
def check_for_updates(current_version: str) -> str:
"""
Check if a new version is available using semantic versioning.
Returns:
Formatted status string for display
"""
try:
response = requests.get(
'https://gitlab.pm/api/v1/repos/rune/oai/releases/latest',
headers={"Content-Type": "application/json"},
timeout=1.0,
allow_redirects=True
)
response.raise_for_status()
data = response.json()
version_online = data.get('tag_name', '').lstrip('v')
if not version_online:
logger.warning("No version found in API response")
return f"[bold green]oAI version {current_version}[/]"
current = pkg_version.parse(current_version)
latest = pkg_version.parse(version_online)
if latest > current:
logger.info(f"Update available: {current_version}{version_online}")
return f"[bold green]oAI version {current_version} [/][bold red](Update available: {current_version}{version_online})[/]"
else:
logger.debug(f"Already up to date: {current_version}")
return f"[bold green]oAI version {current_version} (up to date)[/]"
except requests.exceptions.HTTPError as e:
logger.warning(f"HTTP error checking for updates: {e.response.status_code}")
return f"[bold green]oAI version {current_version}[/]"
except requests.exceptions.ConnectionError:
logger.warning("Network error checking for updates (offline?)")
return f"[bold green]oAI version {current_version}[/]"
except requests.exceptions.Timeout:
logger.warning("Timeout checking for updates")
return f"[bold green]oAI version {current_version}[/]"
except requests.exceptions.RequestException as e:
logger.warning(f"Request error checking for updates: {type(e).__name__}")
return f"[bold green]oAI version {current_version}[/]"
except (KeyError, ValueError) as e:
logger.warning(f"Invalid API response checking for updates: {e}")
return f"[bold green]oAI version {current_version}[/]"
except Exception as e:
logger.error(f"Unexpected error checking for updates: {e}")
return f"[bold green]oAI version {current_version}[/]"
def save_conversation(name: str, data: List[Dict[str, str]]): def save_conversation(name: str, data: List[Dict[str, str]]):
"""Save conversation history to DB.""" """Save conversation history to DB."""
timestamp = datetime.datetime.now().isoformat() timestamp = datetime.datetime.now().isoformat()
@@ -309,6 +415,7 @@ STREAM_ENABLED = get_config('stream_enabled') or "on"
DEFAULT_MODEL_ID = get_config('default_model') DEFAULT_MODEL_ID = get_config('default_model')
MAX_TOKEN = int(get_config('max_token') or "100000") MAX_TOKEN = int(get_config('max_token') or "100000")
COST_WARNING_THRESHOLD = float(get_config(HIGH_COST_WARNING) or "0.01") # Configurable cost threshold for alerts COST_WARNING_THRESHOLD = float(get_config(HIGH_COST_WARNING) or "0.01") # Configurable cost threshold for alerts
DEFAULT_ONLINE_MODE = get_config('default_online_mode') or "off" # New: Default online mode setting
# Fetch models with app identification headers # Fetch models with app identification headers
models_data = [] models_data = []
@@ -416,16 +523,15 @@ def display_paginated_table(table: Table, title: str):
return return
# Separate header from data rows # Separate header from data rows
# Typically the first 3 lines are: top border, header row, separator
header_lines = [] header_lines = []
data_lines = [] data_lines = []
# Find where the header ends (usually after the first horizontal line after header text) # Find where the header ends
header_end_index = 0 header_end_index = 0
found_header_text = False found_header_text = False
for i, line_segments in enumerate(all_lines): for i, line_segments in enumerate(all_lines):
# Check if this line contains header-style text (bold/magenta usually) # Check if this line contains header-style text
has_header_style = any( has_header_style = any(
seg.style and ('bold' in str(seg.style) or 'magenta' in str(seg.style)) seg.style and ('bold' in str(seg.style) or 'magenta' in str(seg.style))
for seg in line_segments for seg in line_segments
@@ -437,21 +543,20 @@ def display_paginated_table(table: Table, title: str):
# After finding header text, the next line with box-drawing chars is the separator # After finding header text, the next line with box-drawing chars is the separator
if found_header_text and i > 0: if found_header_text and i > 0:
line_text = ''.join(seg.text for seg in line_segments) line_text = ''.join(seg.text for seg in line_segments)
# Check for horizontal line characters (─ ━ ╌ etc.)
if any(char in line_text for char in ['', '', '', '', '', '']): if any(char in line_text for char in ['', '', '', '', '', '']):
header_end_index = i header_end_index = i
break break
# If we found a header separator, split there # If we found a header separator, split there
if header_end_index > 0: if header_end_index > 0:
header_lines = all_lines[:header_end_index + 1] # Include the separator header_lines = all_lines[:header_end_index + 1]
data_lines = all_lines[header_end_index + 1:] data_lines = all_lines[header_end_index + 1:]
else: else:
# Fallback: assume first 3 lines are header # Fallback: assume first 3 lines are header
header_lines = all_lines[:min(3, len(all_lines))] header_lines = all_lines[:min(3, len(all_lines))]
data_lines = all_lines[min(3, len(all_lines)):] data_lines = all_lines[min(3, len(all_lines)):]
# Calculate how many data lines fit per page (accounting for header) # Calculate how many data lines fit per page
lines_per_page = terminal_height - len(header_lines) lines_per_page = terminal_height - len(header_lines)
# Display with pagination # Display with pagination
@@ -469,7 +574,7 @@ def display_paginated_table(table: Table, title: str):
for line_segments in header_lines: for line_segments in header_lines:
for segment in line_segments: for segment in line_segments:
console.print(segment.text, style=segment.style, end="") console.print(segment.text, style=segment.style, end="")
console.print() # New line after each row console.print()
# Calculate how many data lines to show on this page # Calculate how many data lines to show on this page
end_line = min(current_line + lines_per_page, len(data_lines)) end_line = min(current_line + lines_per_page, len(data_lines))
@@ -478,7 +583,7 @@ def display_paginated_table(table: Table, title: str):
for line_segments in data_lines[current_line:end_line]: for line_segments in data_lines[current_line:end_line]:
for segment in line_segments: for segment in line_segments:
console.print(segment.text, style=segment.style, end="") console.print(segment.text, style=segment.style, end="")
console.print() # New line after each row console.print()
# Update position # Update position
current_line = end_line current_line = end_line
@@ -492,20 +597,16 @@ def display_paginated_table(table: Table, title: str):
import tty import tty
import termios import termios
# Save terminal settings
fd = sys.stdin.fileno() fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd) old_settings = termios.tcgetattr(fd)
try: try:
# Set terminal to raw mode to read single character
tty.setraw(fd) tty.setraw(fd)
char = sys.stdin.read(1) char = sys.stdin.read(1)
# If not space, break pagination
if char != ' ': if char != ' ':
break break
finally: finally:
# Restore terminal settings
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
except: except:
# Fallback for Windows or if termios not available # Fallback for Windows or if termios not available
@@ -513,12 +614,11 @@ def display_paginated_table(table: Table, title: str):
if input_char != '': if input_char != '':
break break
else: else:
# No more content
break break
@app.command() @app.command()
def chat(): def chat():
global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT
session_max_token = 0 session_max_token = 0
session_system_prompt = "" session_system_prompt = ""
session_history = [] session_history = []
@@ -531,9 +631,9 @@ def chat():
conversation_memory_enabled = True # Memory ON by default conversation_memory_enabled = True # Memory ON by default
memory_start_index = 0 # Track when memory was last enabled memory_start_index = 0 # Track when memory was last enabled
saved_conversations_cache = [] # Cache for /list results to use with /load by number saved_conversations_cache = [] # Cache for /list results to use with /load by number
online_mode_enabled = False # Online mode (web search) disabled by default online_mode_enabled = DEFAULT_ONLINE_MODE == "on" # Initialize from config
app_logger.info("Starting new chat session with memory enabled") # Log session start app_logger.info("Starting new chat session with memory enabled")
if not API_KEY: if not API_KEY:
console.print("[bold red]API key not found. Use '/config api'.[/]") console.print("[bold red]API key not found. Use '/config api'.[/]")
@@ -567,7 +667,8 @@ def chat():
client = OpenRouter(api_key=API_KEY) client = OpenRouter(api_key=API_KEY)
if selected_model: if selected_model:
console.print(f"[bold blue]Welcome to oAI![/] [bold red]Active model: {selected_model['name']}[/]") online_status = "enabled" if online_mode_enabled else "disabled"
console.print(f"[bold blue]Welcome to oAI![/] [bold red]Active model: {selected_model['name']}[/] [dim cyan](Online mode: {online_status})[/]")
else: else:
console.print("[bold blue]Welcome to oAI![/] [italic blue]Select a model with '/model'.[/]") console.print("[bold blue]Welcome to oAI![/] [italic blue]Select a model with '/model'.[/]")
@@ -584,14 +685,11 @@ def chat():
# Handle // escape sequence - convert to single / and treat as regular text # Handle // escape sequence - convert to single / and treat as regular text
if user_input.startswith("//"): if user_input.startswith("//"):
user_input = user_input[1:] # Remove first slash, keep the rest user_input = user_input[1:] # Remove first slash, keep the rest
# Don't process as command, jump to message processing
# Check for unknown commands (starts with / but not a valid command) # Check for unknown commands
elif user_input.startswith("/") and user_input.lower() not in ["exit", "quit", "bye"]: elif user_input.startswith("/") and user_input.lower() not in ["exit", "quit", "bye"]:
# Extract command (first word after /)
command_word = user_input.split()[0].lower() if user_input.split() else user_input.lower() command_word = user_input.split()[0].lower() if user_input.split() else user_input.lower()
# Check if it's a valid command or partial match
if not any(command_word.startswith(cmd) for cmd in VALID_COMMANDS): if not any(command_word.startswith(cmd) for cmd in VALID_COMMANDS):
console.print(f"[bold red]Unknown command: {command_word}[/]") console.print(f"[bold red]Unknown command: {command_word}[/]")
console.print("[bold yellow]Type /help to see all available commands.[/]") console.print("[bold yellow]Type /help to see all available commands.[/]")
@@ -600,7 +698,7 @@ def chat():
if user_input.lower() in ["exit", "quit", "bye"]: if user_input.lower() in ["exit", "quit", "bye"]:
total_tokens = total_input_tokens + total_output_tokens total_tokens = total_input_tokens + total_output_tokens
app_logger.info(f"Session ended. Total messages: {message_count}, Total tokens: {total_tokens}, Total cost: ${total_cost:.4f}") # Log session summary app_logger.info(f"Session ended. Total messages: {message_count}, Total tokens: {total_tokens}, Total cost: ${total_cost:.4f}")
console.print("[bold yellow]Goodbye![/]") console.print("[bold yellow]Goodbye![/]")
return return
@@ -618,7 +716,9 @@ def chat():
args = user_input[8:].strip() args = user_input[8:].strip()
if not args: if not args:
status = "enabled" if online_mode_enabled else "disabled" status = "enabled" if online_mode_enabled else "disabled"
default_status = "enabled" if DEFAULT_ONLINE_MODE == "on" else "disabled"
console.print(f"[bold blue]Online mode (web search) {status}.[/]") console.print(f"[bold blue]Online mode (web search) {status}.[/]")
console.print(f"[dim blue]Default setting: {default_status} (use '/config online on|off' to change)[/]")
if selected_model: if selected_model:
if supports_online_mode(selected_model): if supports_online_mode(selected_model):
console.print(f"[dim green]Current model '{selected_model['name']}' supports online mode.[/]") console.print(f"[dim green]Current model '{selected_model['name']}' supports online mode.[/]")
@@ -635,12 +735,12 @@ def chat():
app_logger.warning(f"Online mode activation failed - model {selected_model['id']} doesn't support it") app_logger.warning(f"Online mode activation failed - model {selected_model['id']} doesn't support it")
continue continue
online_mode_enabled = True online_mode_enabled = True
console.print("[bold green]Online mode enabled. Model will use web search capabilities.[/]") console.print("[bold green]Online mode enabled for this session. Model will use web search capabilities.[/]")
console.print(f"[dim blue]Effective model ID: {get_effective_model_id(selected_model['id'], True)}[/]") console.print(f"[dim blue]Effective model ID: {get_effective_model_id(selected_model['id'], True)}[/]")
app_logger.info(f"Online mode enabled for model {selected_model['id']}") app_logger.info(f"Online mode enabled for model {selected_model['id']}")
elif args.lower() == "off": elif args.lower() == "off":
online_mode_enabled = False online_mode_enabled = False
console.print("[bold green]Online mode disabled. Model will not use web search.[/]") console.print("[bold green]Online mode disabled for this session. Model will not use web search.[/]")
if selected_model: if selected_model:
console.print(f"[dim blue]Effective model ID: {selected_model['id']}[/]") console.print(f"[dim blue]Effective model ID: {selected_model['id']}[/]")
app_logger.info("Online mode disabled") app_logger.info("Online mode disabled")
@@ -660,7 +760,7 @@ def chat():
continue continue
if args.lower() == "on": if args.lower() == "on":
conversation_memory_enabled = True conversation_memory_enabled = True
memory_start_index = len(session_history) # Remember where we started memory_start_index = len(session_history)
console.print("[bold green]Conversation memory enabled. Will remember conversations from this point forward.[/]") console.print("[bold green]Conversation memory enabled. Will remember conversations from this point forward.[/]")
console.print(f"[dim blue]Memory will track messages starting from index {memory_start_index}.[/]") console.print(f"[dim blue]Memory will track messages starting from index {memory_start_index}.[/]")
app_logger.info(f"Conversation memory enabled at index {memory_start_index}") app_logger.info(f"Conversation memory enabled at index {memory_start_index}")
@@ -673,7 +773,6 @@ def chat():
console.print("[bold yellow]Usage: /memory on|off (or /memory to view status)[/]") console.print("[bold yellow]Usage: /memory on|off (or /memory to view status)[/]")
continue continue
elif user_input.lower().startswith("/paste"): elif user_input.lower().startswith("/paste"):
# Get optional prompt after /paste
optional_prompt = user_input[7:].strip() optional_prompt = user_input[7:].strip()
try: try:
@@ -688,13 +787,10 @@ def chat():
app_logger.warning("Paste attempted with empty clipboard") app_logger.warning("Paste attempted with empty clipboard")
continue continue
# Validate it's text (check if it's valid UTF-8 and printable)
try: try:
# Try to encode/decode to ensure it's valid text
clipboard_content.encode('utf-8') clipboard_content.encode('utf-8')
# Show preview of pasted content preview_lines = clipboard_content.split('\n')[:10]
preview_lines = clipboard_content.split('\n')[:10] # First 10 lines
preview_text = '\n'.join(preview_lines) preview_text = '\n'.join(preview_lines)
if len(clipboard_content.split('\n')) > 10: if len(clipboard_content.split('\n')) > 10:
preview_text += "\n... (content truncated for preview)" preview_text += "\n... (content truncated for preview)"
@@ -709,7 +805,6 @@ def chat():
border_style="cyan" border_style="cyan"
)) ))
# Build the final prompt
if optional_prompt: if optional_prompt:
final_prompt = f"{optional_prompt}\n\n```\n{clipboard_content}\n```" final_prompt = f"{optional_prompt}\n\n```\n{clipboard_content}\n```"
console.print(f"[dim blue]Sending with prompt: '{optional_prompt}'[/]") console.print(f"[dim blue]Sending with prompt: '{optional_prompt}'[/]")
@@ -717,7 +812,6 @@ def chat():
final_prompt = clipboard_content final_prompt = clipboard_content
console.print("[dim blue]Sending clipboard content without additional prompt[/]") console.print("[dim blue]Sending clipboard content without additional prompt[/]")
# Set user_input to the pasted content so it gets processed normally
user_input = final_prompt user_input = final_prompt
app_logger.info(f"Pasted content from clipboard: {char_count} chars, {line_count} lines, with prompt: {bool(optional_prompt)}") app_logger.info(f"Pasted content from clipboard: {char_count} chars, {line_count} lines, with prompt: {bool(optional_prompt)}")
@@ -745,13 +839,11 @@ def chat():
console.print("[bold red]No conversation history to export.[/]") console.print("[bold red]No conversation history to export.[/]")
continue continue
# Validate format
if export_format not in ['md', 'json', 'html']: if export_format not in ['md', 'json', 'html']:
console.print("[bold red]Invalid format. Use: md, json, or html[/]") console.print("[bold red]Invalid format. Use: md, json, or html[/]")
continue continue
try: try:
# Generate export content
if export_format == 'md': if export_format == 'md':
content = export_as_markdown(session_history, session_system_prompt) content = export_as_markdown(session_history, session_system_prompt)
elif export_format == 'json': elif export_format == 'json':
@@ -759,7 +851,6 @@ def chat():
elif export_format == 'html': elif export_format == 'html':
content = export_as_html(session_history, session_system_prompt) content = export_as_html(session_history, session_system_prompt)
# Write to file
export_path = Path(filename).expanduser() export_path = Path(filename).expanduser()
with open(export_path, 'w', encoding='utf-8') as f: with open(export_path, 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
@@ -790,7 +881,6 @@ def chat():
console.print("[bold yellow]Tip: Use /list to see numbered conversations[/]") console.print("[bold yellow]Tip: Use /list to see numbered conversations[/]")
continue continue
# Check if input is a number
conversation_name = None conversation_name = None
if args.isdigit(): if args.isdigit():
conv_number = int(args) conv_number = int(args)
@@ -811,9 +901,8 @@ def chat():
continue continue
session_history = loaded_data session_history = loaded_data
current_index = len(session_history) - 1 current_index = len(session_history) - 1
# When loading, reset memory tracking if memory is enabled
if conversation_memory_enabled: if conversation_memory_enabled:
memory_start_index = 0 # Include all loaded messages in memory memory_start_index = 0
total_input_tokens = 0 total_input_tokens = 0
total_output_tokens = 0 total_output_tokens = 0
total_cost = 0.0 total_cost = 0.0
@@ -828,7 +917,6 @@ def chat():
console.print("[bold yellow]Tip: Use /list to see numbered conversations[/]") console.print("[bold yellow]Tip: Use /list to see numbered conversations[/]")
continue continue
# Check if input is a number
conversation_name = None conversation_name = None
if args.isdigit(): if args.isdigit():
conv_number = int(args) conv_number = int(args)
@@ -842,7 +930,6 @@ def chat():
else: else:
conversation_name = args conversation_name = args
# Confirm deletion
try: try:
confirm = typer.confirm(f"Delete conversation '{conversation_name}'? This cannot be undone.", default=False) confirm = typer.confirm(f"Delete conversation '{conversation_name}'? This cannot be undone.", default=False)
if not confirm: if not confirm:
@@ -856,7 +943,6 @@ def chat():
if deleted_count > 0: if deleted_count > 0:
console.print(f"[bold green]Conversation '{conversation_name}' deleted ({deleted_count} version(s) removed).[/]") console.print(f"[bold green]Conversation '{conversation_name}' deleted ({deleted_count} version(s) removed).[/]")
app_logger.info(f"Conversation '{conversation_name}' deleted - {deleted_count} version(s)") app_logger.info(f"Conversation '{conversation_name}' deleted - {deleted_count} version(s)")
# Refresh cache if deleted conversation was in it
if saved_conversations_cache: if saved_conversations_cache:
saved_conversations_cache = [c for c in saved_conversations_cache if c['name'] != conversation_name] saved_conversations_cache = [c for c in saved_conversations_cache if c['name'] != conversation_name]
else: else:
@@ -871,12 +957,10 @@ def chat():
saved_conversations_cache = [] saved_conversations_cache = []
continue continue
# Update cache for /load command
saved_conversations_cache = conversations saved_conversations_cache = conversations
table = Table("No.", "Name", "Messages", "Last Saved", show_header=True, header_style="bold magenta") table = Table("No.", "Name", "Messages", "Last Saved", show_header=True, header_style="bold magenta")
for idx, conv in enumerate(conversations, 1): for idx, conv in enumerate(conversations, 1):
# Parse ISO timestamp and format it nicely
try: try:
dt = datetime.datetime.fromisoformat(conv['timestamp']) dt = datetime.datetime.fromisoformat(conv['timestamp'])
formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S') formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S')
@@ -899,7 +983,6 @@ def chat():
continue continue
current_index -= 1 current_index -= 1
prev_response = session_history[current_index]['response'] prev_response = session_history[current_index]['response']
# Render as markdown with proper formatting
md = Markdown(prev_response) md = Markdown(prev_response)
console.print(Panel(md, title=f"[bold green]Previous Response ({current_index + 1}/{len(session_history)})[/]", title_align="left")) console.print(Panel(md, title=f"[bold green]Previous Response ({current_index + 1}/{len(session_history)})[/]", title_align="left"))
app_logger.debug(f"Viewed previous response at index {current_index}") app_logger.debug(f"Viewed previous response at index {current_index}")
@@ -910,7 +993,6 @@ def chat():
continue continue
current_index += 1 current_index += 1
next_response = session_history[current_index]['response'] next_response = session_history[current_index]['response']
# Render as markdown with proper formatting
md = Markdown(next_response) md = Markdown(next_response)
console.print(Panel(md, title=f"[bold green]Next Response ({current_index + 1}/{len(session_history)})[/]", title_align="left")) console.print(Panel(md, title=f"[bold green]Next Response ({current_index + 1}/{len(session_history)})[/]", title_align="left"))
app_logger.debug(f"Viewed next response at index {current_index}") app_logger.debug(f"Viewed next response at index {current_index}")
@@ -925,7 +1007,6 @@ def chat():
console.print(Panel(table, title="[bold green]Session Cost Summary[/]", title_align="left")) console.print(Panel(table, title="[bold green]Session Cost Summary[/]", title_align="left"))
app_logger.info(f"User viewed stats: {stats}") app_logger.info(f"User viewed stats: {stats}")
# Cost warnings in /stats
warnings = check_credit_alerts(credits) warnings = check_credit_alerts(credits)
if warnings: if warnings:
warning_text = '|'.join(warnings) warning_text = '|'.join(warnings)
@@ -954,7 +1035,7 @@ def chat():
session_history = [] session_history = []
current_index = -1 current_index = -1
session_system_prompt = "" session_system_prompt = ""
memory_start_index = 0 # Reset memory tracking memory_start_index = 0
total_input_tokens = 0 total_input_tokens = 0
total_output_tokens = 0 total_output_tokens = 0
total_cost = 0.0 total_cost = 0.0
@@ -976,7 +1057,6 @@ def chat():
console.print(f"[bold red]Model '{args}' not found.[/]") console.print(f"[bold red]Model '{args}' not found.[/]")
continue continue
# Display model info
pricing = model_to_show.get("pricing", {}) pricing = model_to_show.get("pricing", {})
architecture = model_to_show.get("architecture", {}) architecture = model_to_show.get("architecture", {})
supported_params = ", ".join(model_to_show.get("supported_parameters", [])) or "None" supported_params = ", ".join(model_to_show.get("supported_parameters", [])) or "None"
@@ -1002,7 +1082,6 @@ def chat():
console.print(Panel(table, title=f"[bold green]Model Info: {model_to_show['name']}[/]", title_align="left")) console.print(Panel(table, title=f"[bold green]Model Info: {model_to_show['name']}[/]", title_align="left"))
continue continue
# Model selection with Image and Online columns
elif user_input.startswith("/model"): elif user_input.startswith("/model"):
app_logger.info("User initiated model selection") app_logger.info("User initiated model selection")
args = user_input[7:].strip() args = user_input[7:].strip()
@@ -1014,14 +1093,12 @@ def chat():
console.print(f"[bold red]No models match '{search_term}'. Try '/model'.[/]") console.print(f"[bold red]No models match '{search_term}'. Try '/model'.[/]")
continue continue
# Create table with Image and Online columns
table = Table("No.", "Name", "ID", "Image", "Online", show_header=True, header_style="bold magenta") table = Table("No.", "Name", "ID", "Image", "Online", show_header=True, header_style="bold magenta")
for i, model in enumerate(filtered_models, 1): for i, model in enumerate(filtered_models, 1):
image_support = "[green]✓[/green]" if has_image_capability(model) else "[red]✗[/red]" image_support = "[green]✓[/green]" if has_image_capability(model) else "[red]✗[/red]"
online_support = "[green]✓[/green]" if supports_online_mode(model) else "[red]✗[/red]" online_support = "[green]✓[/green]" if supports_online_mode(model) else "[red]✗[/red]"
table.add_row(str(i), model["name"], model["id"], image_support, online_support) table.add_row(str(i), model["name"], model["id"], image_support, online_support)
# Use pagination for the table
title = f"[bold green]Available Models ({'All' if not search_term else f'Search: {search_term}'})[/]" title = f"[bold green]Available Models ({'All' if not search_term else f'Search: {search_term}'})[/]"
display_paginated_table(table, title) display_paginated_table(table, title)
@@ -1032,14 +1109,19 @@ def chat():
break break
if 1 <= choice <= len(filtered_models): if 1 <= choice <= len(filtered_models):
selected_model = filtered_models[choice - 1] selected_model = filtered_models[choice - 1]
# Disable online mode when switching models (user must re-enable)
if online_mode_enabled: # Apply default online mode if model supports it
if supports_online_mode(selected_model) and DEFAULT_ONLINE_MODE == "on":
online_mode_enabled = True
console.print(f"[bold cyan]Selected: {selected_model['name']} ({selected_model['id']})[/]")
console.print("[dim cyan]✓ Online mode auto-enabled (default setting). Use '/online off' to disable for this session.[/]")
else:
online_mode_enabled = False online_mode_enabled = False
console.print("[dim yellow]Note: Online mode auto-disabled when changing models.[/]")
console.print(f"[bold cyan]Selected: {selected_model['name']} ({selected_model['id']})[/]") console.print(f"[bold cyan]Selected: {selected_model['name']} ({selected_model['id']})[/]")
if supports_online_mode(selected_model): if supports_online_mode(selected_model):
console.print("[dim green]✓ This model supports online mode. Use '/online on' to enable web search.[/]") console.print("[dim green]✓ This model supports online mode. Use '/online on' to enable web search.[/]")
app_logger.info(f"Model selected: {selected_model['name']} ({selected_model['id']})")
app_logger.info(f"Model selected: {selected_model['name']} ({selected_model['id']}), Online: {online_mode_enabled}")
break break
console.print("[bold red]Invalid choice. Try again.[/]") console.print("[bold red]Invalid choice. Try again.[/]")
except ValueError: except ValueError:
@@ -1047,7 +1129,6 @@ def chat():
continue continue
elif user_input.startswith("/maxtoken"): elif user_input.startswith("/maxtoken"):
# (unchanged)
args = user_input[10:].strip() args = user_input[10:].strip()
if not args: if not args:
console.print(f"[bold blue]Current session max tokens: {session_max_token}[/]") console.print(f"[bold blue]Current session max tokens: {session_max_token}[/]")
@@ -1067,7 +1148,6 @@ def chat():
continue continue
elif user_input.startswith("/system"): elif user_input.startswith("/system"):
# (unchanged but added to history view)
args = user_input[8:].strip() args = user_input[8:].strip()
if not args: if not args:
if session_system_prompt: if session_system_prompt:
@@ -1085,13 +1165,14 @@ def chat():
elif user_input.startswith("/config"): elif user_input.startswith("/config"):
args = user_input[8:].strip().lower() args = user_input[8:].strip().lower()
update = check_for_updates(version)
if args == "api": if args == "api":
try: try:
new_api_key = typer.prompt("Enter new API key") new_api_key = typer.prompt("Enter new API key")
if new_api_key.strip(): if new_api_key.strip():
set_config('api_key', new_api_key.strip()) set_config('api_key', new_api_key.strip())
API_KEY = new_api_key.strip() API_KEY = new_api_key.strip()
# Reinitialize client with new API key
client = OpenRouter(api_key=API_KEY) client = OpenRouter(api_key=API_KEY)
console.print("[bold green]API key updated![/]") console.print("[bold green]API key updated![/]")
else: else:
@@ -1110,7 +1191,7 @@ def chat():
except Exception as e: except Exception as e:
console.print(f"[bold red]Error updating URL: {e}[/]") console.print(f"[bold red]Error updating URL: {e}[/]")
elif args.startswith("costwarning"): elif args.startswith("costwarning"):
sub_args = args[11:].strip() # Extract everything after "costwarning" sub_args = args[11:].strip()
if not sub_args: if not sub_args:
console.print(f"[bold blue]Stored cost warning threshold: ${COST_WARNING_THRESHOLD:.4f}[/]") console.print(f"[bold blue]Stored cost warning threshold: ${COST_WARNING_THRESHOLD:.4f}[/]")
continue continue
@@ -1132,6 +1213,43 @@ def chat():
console.print(f"[bold green]Streaming {'enabled' if sub_args == 'on' else 'disabled'}.[/]") console.print(f"[bold green]Streaming {'enabled' if sub_args == 'on' else 'disabled'}.[/]")
else: else:
console.print("[bold yellow]Usage: /config stream on|off[/]") console.print("[bold yellow]Usage: /config stream on|off[/]")
elif args.startswith("log"):
sub_args = args[4:].strip()
if not sub_args:
console.print(f"[bold blue]Current log file size limit: {LOG_MAX_SIZE_MB} MB[/]")
console.print(f"[bold blue]Log backup count: {LOG_BACKUP_COUNT} files[/]")
console.print(f"[dim yellow]Total max disk usage: ~{LOG_MAX_SIZE_MB * (LOG_BACKUP_COUNT + 1)} MB[/]")
continue
try:
new_size_mb = int(sub_args)
if new_size_mb < 1:
console.print("[bold red]Log size must be at least 1 MB.[/]")
continue
if new_size_mb > 100:
console.print("[bold yellow]Warning: Log size > 100MB. Capping at 100MB.[/]")
new_size_mb = 100
set_config('log_max_size_mb', str(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.[/]")
app_logger.info(f"Log size limit updated to {new_size_mb} MB (requires restart)")
except ValueError:
console.print("[bold red]Invalid size. Provide a number in MB.[/]")
elif args.startswith("online"):
sub_args = args[7:].strip()
if not sub_args:
current_default = "enabled" if DEFAULT_ONLINE_MODE == "on" else "disabled"
console.print(f"[bold blue]Default online mode: {current_default}[/]")
console.print("[dim yellow]This sets the default for new models. Use '/online on|off' to override in current session.[/]")
continue
if sub_args in ["on", "off"]:
set_config('default_online_mode', sub_args)
DEFAULT_ONLINE_MODE = sub_args
console.print(f"[bold green]Default online mode {'enabled' if sub_args == 'on' else 'disabled'}.[/]")
console.print("[dim blue]Note: This affects new model selections. Current session unchanged.[/]")
app_logger.info(f"Default online mode set to {sub_args}")
else:
console.print("[bold yellow]Usage: /config online on|off[/]")
elif args.startswith("maxtoken"): elif args.startswith("maxtoken"):
sub_args = args[9:].strip() sub_args = args[9:].strip()
if not sub_args: if not sub_args:
@@ -1163,14 +1281,12 @@ def chat():
console.print(f"[bold red]No models match '{search_term}'. Try without search.[/]") console.print(f"[bold red]No models match '{search_term}'. Try without search.[/]")
continue continue
# Create table with Image and Online columns
table = Table("No.", "Name", "ID", "Image", "Online", show_header=True, header_style="bold magenta") table = Table("No.", "Name", "ID", "Image", "Online", show_header=True, header_style="bold magenta")
for i, model in enumerate(filtered_models, 1): for i, model in enumerate(filtered_models, 1):
image_support = "[green]✓[/green]" if has_image_capability(model) else "[red]✗[/red]" image_support = "[green]✓[/green]" if has_image_capability(model) else "[red]✗[/red]"
online_support = "[green]✓[/green]" if supports_online_mode(model) else "[red]✗[/red]" online_support = "[green]✓[/green]" if supports_online_mode(model) else "[red]✗[/red]"
table.add_row(str(i), model["name"], model["id"], image_support, online_support) table.add_row(str(i), model["name"], model["id"], image_support, online_support)
# Use pagination for the table
title = f"[bold green]Available Models for Default ({'All' if not search_term else f'Search: {search_term}'})[/]" title = f"[bold green]Available Models for Default ({'All' if not search_term else f'Search: {search_term}'})[/]"
display_paginated_table(table, title) display_paginated_table(table, title)
@@ -1197,10 +1313,13 @@ def chat():
table.add_row("Base URL", OPENROUTER_BASE_URL or "[Not set]") table.add_row("Base URL", OPENROUTER_BASE_URL or "[Not set]")
table.add_row("DB Path", str(database) or "[Not set]") table.add_row("DB Path", str(database) or "[Not set]")
table.add_row("Logfile", str(log_file) or "[Not set]") table.add_row("Logfile", str(log_file) or "[Not set]")
table.add_row("Log Size Limit", f"{LOG_MAX_SIZE_MB} MB")
table.add_row("Log Backups", str(LOG_BACKUP_COUNT))
table.add_row("Streaming", "Enabled" if STREAM_ENABLED == "on" else "Disabled") table.add_row("Streaming", "Enabled" if STREAM_ENABLED == "on" else "Disabled")
table.add_row("Default Model", DEFAULT_MODEL_ID or "[Not set]") table.add_row("Default Model", DEFAULT_MODEL_ID or "[Not set]")
table.add_row("Current Model", "[Not set]" if selected_model is None else str(selected_model["name"])) table.add_row("Current Model", "[Not set]" if selected_model is None else str(selected_model["name"]))
table.add_row("Online Mode", "Enabled" if online_mode_enabled else "Disabled") table.add_row("Default Online Mode", "Enabled" if DEFAULT_ONLINE_MODE == "on" else "Disabled")
table.add_row("Session Online Mode", "Enabled" if online_mode_enabled else "Disabled")
table.add_row("Max Token", str(MAX_TOKEN)) table.add_row("Max Token", str(MAX_TOKEN))
table.add_row("Session Token", "[Not set]" if session_max_token == 0 else str(session_max_token)) table.add_row("Session Token", "[Not set]" if session_max_token == 0 else str(session_max_token))
table.add_row("Session System Prompt", session_system_prompt or "[Not set]") table.add_row("Session System Prompt", session_system_prompt or "[Not set]")
@@ -1222,7 +1341,7 @@ def chat():
table.add_row("Used Credits", "[Unavailable - Check API key]") table.add_row("Used Credits", "[Unavailable - Check API key]")
table.add_row("Credits Left", "[Unavailable - Check API key]") table.add_row("Credits Left", "[Unavailable - Check API key]")
console.print(Panel(table, title="[bold green]Current Configurations[/]", title_align="left", subtitle="[bold green]oAI Version %s" % version, subtitle_align="right")) console.print(Panel(table, title="[bold green]Current Configurations[/]", title_align="left", subtitle="%s" %(update), subtitle_align="right"))
continue continue
if user_input.lower() == "/credits": if user_input.lower() == "/credits":
@@ -1250,7 +1369,7 @@ def chat():
if user_input.lower() == "/help": if user_input.lower() == "/help":
help_table = Table("Command", "Description", "Example", show_header=True, header_style="bold cyan", width=console.width - 10) help_table = Table("Command", "Description", "Example", show_header=True, header_style="bold cyan", width=console.width - 10)
# ===== SESSION COMMANDS ===== # SESSION COMMANDS
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ SESSION COMMANDS ━━━[/]", "[bold yellow]━━━ SESSION COMMANDS ━━━[/]",
"", "",
@@ -1278,7 +1397,7 @@ def chat():
) )
help_table.add_row( help_table.add_row(
"/online [on|off]", "/online [on|off]",
"Enable/disable online mode (web search) for current model. Only works with models that support tools.", "Enable/disable online mode (web search) for current session. Overrides default setting.",
"/online on\n/online off" "/online on\n/online off"
) )
help_table.add_row( help_table.add_row(
@@ -1302,7 +1421,7 @@ def chat():
"/retry" "/retry"
) )
# ===== MODEL COMMANDS ===== # MODEL COMMANDS
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ MODEL COMMANDS ━━━[/]", "[bold yellow]━━━ MODEL COMMANDS ━━━[/]",
"", "",
@@ -1319,7 +1438,7 @@ def chat():
"/model\n/model gpt" "/model\n/model gpt"
) )
# ===== CONFIGURATION COMMANDS (ALPHABETICAL) ===== # CONFIGURATION COMMANDS
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ CONFIGURATION ━━━[/]", "[bold yellow]━━━ CONFIGURATION ━━━[/]",
"", "",
@@ -1340,6 +1459,11 @@ def chat():
"Set the cost warning threshold. Alerts when response exceeds this cost (in USD).", "Set the cost warning threshold. Alerts when response exceeds this cost (in USD).",
"/config costwarning 0.05" "/config costwarning 0.05"
) )
help_table.add_row(
"/config log [size_mb]",
"Set log file size limit in MB. Older logs are rotated automatically. Requires restart.",
"/config log 20"
)
help_table.add_row( help_table.add_row(
"/config maxtoken [value]", "/config maxtoken [value]",
"Set stored max token limit (persisted in DB). View current if no value provided.", "Set stored max token limit (persisted in DB). View current if no value provided.",
@@ -1350,6 +1474,11 @@ def chat():
"Set default model that loads on startup. Shows image and online capabilities. Doesn't change current session model.", "Set default model that loads on startup. Shows image and online capabilities. Doesn't change current session model.",
"/config model gpt" "/config model gpt"
) )
help_table.add_row(
"/config online [on|off]",
"Set default online mode for new model selections. Use '/online on|off' to override current session.",
"/config online on"
)
help_table.add_row( help_table.add_row(
"/config stream [on|off]", "/config stream [on|off]",
"Enable or disable response streaming.", "Enable or disable response streaming.",
@@ -1361,7 +1490,7 @@ def chat():
"/config url" "/config url"
) )
# ===== TOKEN & SYSTEM COMMANDS ===== # TOKEN & SYSTEM COMMANDS
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ TOKEN & SYSTEM ━━━[/]", "[bold yellow]━━━ TOKEN & SYSTEM ━━━[/]",
"", "",
@@ -1383,7 +1512,7 @@ def chat():
"/system You are a Python expert" "/system You are a Python expert"
) )
# ===== CONVERSATION MANAGEMENT ===== # CONVERSATION MANAGEMENT
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ CONVERSATION MGMT ━━━[/]", "[bold yellow]━━━ CONVERSATION MGMT ━━━[/]",
"", "",
@@ -1415,7 +1544,7 @@ def chat():
"/save my_chat" "/save my_chat"
) )
# ===== MONITORING & STATS ===== # MONITORING & STATS
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ MONITORING & STATS ━━━[/]", "[bold yellow]━━━ MONITORING & STATS ━━━[/]",
"", "",
@@ -1432,7 +1561,7 @@ def chat():
"/stats" "/stats"
) )
# ===== FILE ATTACHMENTS ===== # INPUT METHODS
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ INPUT METHODS ━━━[/]", "[bold yellow]━━━ INPUT METHODS ━━━[/]",
"", "",
@@ -1454,7 +1583,7 @@ def chat():
"//help sends '/help' as text" "//help sends '/help' as text"
) )
# ===== EXIT ===== # EXIT
help_table.add_row( help_table.add_row(
"[bold yellow]━━━ EXIT ━━━[/]", "[bold yellow]━━━ EXIT ━━━[/]",
"", "",
@@ -1514,7 +1643,6 @@ def chat():
# Handle PDFs # Handle PDFs
elif mime_type == 'application/pdf' or file_ext == '.pdf': elif mime_type == 'application/pdf' or file_ext == '.pdf':
modalities = selected_model.get("architecture", {}).get("input_modalities", []) modalities = selected_model.get("architecture", {}).get("input_modalities", [])
# Check for various possible modality indicators for PDFs
supports_pdf = any(mod in modalities for mod in ["document", "pdf", "file"]) supports_pdf = any(mod in modalities for mod in ["document", "pdf", "file"])
if not supports_pdf: if not supports_pdf:
console.print("[bold red]Selected model does not support PDF attachments.[/]") console.print("[bold red]Selected model does not support PDF attachments.[/]")
@@ -1560,13 +1688,10 @@ def chat():
# Build API messages with conversation history if memory is enabled # Build API messages with conversation history if memory is enabled
api_messages = [] api_messages = []
# Add system prompt if set
if session_system_prompt: if session_system_prompt:
api_messages.append({"role": "system", "content": session_system_prompt}) api_messages.append({"role": "system", "content": session_system_prompt})
# Add conversation history only if memory is enabled (from memory start point onwards)
if conversation_memory_enabled: if conversation_memory_enabled:
# Only include history from when memory was last enabled
for i in range(memory_start_index, len(session_history)): for i in range(memory_start_index, len(session_history)):
history_entry = session_history[i] history_entry = session_history[i]
api_messages.append({ api_messages.append({
@@ -1578,13 +1703,12 @@ def chat():
"content": history_entry['response'] "content": history_entry['response']
}) })
# Add current user message
api_messages.append({"role": "user", "content": message_content}) api_messages.append({"role": "user", "content": message_content})
# Get effective model ID with :online suffix if enabled # Get effective model ID with :online suffix if enabled
effective_model_id = get_effective_model_id(selected_model["id"], online_mode_enabled) effective_model_id = get_effective_model_id(selected_model["id"], online_mode_enabled)
# Build API params with app identification headers (using http_headers) # Build API params
api_params = { api_params = {
"model": effective_model_id, "model": effective_model_id,
"messages": api_messages, "messages": api_messages,
@@ -1606,17 +1730,17 @@ def chat():
online_status = "ON" if online_mode_enabled else "OFF" online_status = "ON" if online_mode_enabled else "OFF"
app_logger.info(f"API Request: Model '{effective_model_id}' (Online: {online_status}), Prompt length: {len(text_part)} chars, {file_count} file(s) attached, Memory: {memory_status}, History sent: {history_messages_count} messages, Transforms: middle-out {'enabled' if middle_out_enabled else 'disabled'}, App: {APP_NAME} ({APP_URL}).") app_logger.info(f"API Request: Model '{effective_model_id}' (Online: {online_status}), Prompt length: {len(text_part)} chars, {file_count} file(s) attached, Memory: {memory_status}, History sent: {history_messages_count} messages, Transforms: middle-out {'enabled' if middle_out_enabled else 'disabled'}, App: {APP_NAME} ({APP_URL}).")
# Send and handle response with metrics and timing # Send and handle response
is_streaming = STREAM_ENABLED == "on" is_streaming = STREAM_ENABLED == "on"
if is_streaming: if is_streaming:
console.print("[bold green]Streaming response...[/] [dim](Press Ctrl+C to cancel)[/]") console.print("[bold green]Streaming response...[/] [dim](Press Ctrl+C to cancel)[/]")
if online_mode_enabled: if online_mode_enabled:
console.print("[dim cyan]🌐 Online mode active - model has web search access[/]") console.print("[dim cyan]🌐 Online mode active - model has web search access[/]")
console.print("") # Add spacing before response console.print("")
else: else:
console.print("[bold green]Thinking...[/]", end="\r") console.print("[bold green]Thinking...[/]", end="\r")
start_time = time.time() # Start timing request start_time = time.time()
try: try:
response = client.chat.send(**api_params) response = client.chat.send(**api_params)
app_logger.info(f"API call successful for model '{effective_model_id}'") app_logger.info(f"API call successful for model '{effective_model_id}'")
@@ -1625,12 +1749,11 @@ def chat():
app_logger.error(f"API Error: {type(e).__name__}: {e}") app_logger.error(f"API Error: {type(e).__name__}: {e}")
continue continue
response_time = time.time() - start_time # Calculate response time response_time = time.time() - start_time
full_response = "" full_response = ""
if is_streaming: if is_streaming:
try: try:
# Use Live display for smooth streaming with proper wrapping
with Live("", console=console, refresh_per_second=10, auto_refresh=True) as live: with Live("", console=console, refresh_per_second=10, auto_refresh=True) as live:
for chunk in response: for chunk in response:
if hasattr(chunk, 'error') and chunk.error: if hasattr(chunk, 'error') and chunk.error:
@@ -1640,11 +1763,9 @@ def chat():
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content: if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content:
content_chunk = chunk.choices[0].delta.content content_chunk = chunk.choices[0].delta.content
full_response += content_chunk full_response += content_chunk
# Update live display with markdown rendering
md = Markdown(full_response) md = Markdown(full_response)
live.update(md) live.update(md)
# Add newline after streaming completes
console.print("") console.print("")
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -1656,15 +1777,14 @@ def chat():
console.print(f"\r{' ' * 20}\r", end="") console.print(f"\r{' ' * 20}\r", end="")
if full_response: if full_response:
# Render response with proper markdown formatting if not is_streaming:
if not is_streaming: # Only show panel for non-streaming (streaming already displayed)
md = Markdown(full_response) md = Markdown(full_response)
console.print(Panel(md, title="[bold green]AI Response[/]", title_align="left", border_style="green")) console.print(Panel(md, title="[bold green]AI Response[/]", title_align="left", border_style="green"))
session_history.append({'prompt': user_input, 'response': full_response}) session_history.append({'prompt': user_input, 'response': full_response})
current_index = len(session_history) - 1 current_index = len(session_history) - 1
# Process metrics for per-message display and session tracking # Process metrics
usage = getattr(response, 'usage', None) usage = getattr(response, 'usage', None)
input_tokens = usage.input_tokens if usage and hasattr(usage, 'input_tokens') else 0 input_tokens = usage.input_tokens if usage and hasattr(usage, 'input_tokens') else 0
output_tokens = usage.output_tokens if usage and hasattr(usage, 'output_tokens') else 0 output_tokens = usage.output_tokens if usage and hasattr(usage, 'output_tokens') else 0
@@ -1675,10 +1795,9 @@ def chat():
total_cost += msg_cost total_cost += msg_cost
message_count += 1 message_count += 1
# Log response metrics
app_logger.info(f"Response: Tokens - I:{input_tokens} O:{output_tokens} T:{input_tokens + output_tokens}, Cost: ${msg_cost:.4f}, Time: {response_time:.2f}s, Online: {online_mode_enabled}") app_logger.info(f"Response: Tokens - I:{input_tokens} O:{output_tokens} T:{input_tokens + output_tokens}, Cost: ${msg_cost:.4f}, Time: {response_time:.2f}s, Online: {online_mode_enabled}")
# Per-message metrics display with context info # Per-message metrics display
if conversation_memory_enabled: if conversation_memory_enabled:
context_count = len(session_history) - memory_start_index context_count = len(session_history) - memory_start_index
context_info = f", Context: {context_count} msg(s)" if context_count > 1 else "" context_info = f", Context: {context_count} msg(s)" if context_count > 1 else ""
@@ -1701,7 +1820,6 @@ def chat():
console.print(f"[bold red]⚠️ {warning_text}[/]") console.print(f"[bold red]⚠️ {warning_text}[/]")
app_logger.warning(f"Warnings triggered: {warning_text}") app_logger.warning(f"Warnings triggered: {warning_text}")
# Add spacing before copy prompt
console.print("") console.print("")
try: try:
copy_choice = input("💾 Type 'c' to copy response, or press Enter to continue: ").strip().lower() copy_choice = input("💾 Type 'c' to copy response, or press Enter to continue: ").strip().lower()
@@ -1711,7 +1829,6 @@ def chat():
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
pass pass
# Add spacing after interaction
console.print("") console.print("")
else: else:
console.print("[bold red]No response received.[/]") console.print("[bold red]No response received.[/]")

View File

@@ -14,8 +14,9 @@ markdown2==2.5.4
mdurl==0.1.2 mdurl==0.1.2
natsort==8.4.0 natsort==8.4.0
openrouter==0.0.19 openrouter==0.0.19
packaging==25.0
pipreqs==0.4.13 pipreqs==0.4.13
prompt_toolkit==3.0.52 prompt-toolkit==3.0.52
Pygments==2.19.2 Pygments==2.19.2
pyperclip==1.11.0 pyperclip==1.11.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
@@ -30,7 +31,7 @@ soupsieve==2.8
svgwrite==1.4.3 svgwrite==1.4.3
tqdm==4.67.1 tqdm==4.67.1
typer==0.20.0 typer==0.20.0
typing_extensions==4.15.0 typing-extensions==4.15.0
urllib3==2.5.0 urllib3==2.5.0
wavedrom==2.0.3.post3 wavedrom==2.0.3.post3
wcwidth==0.2.14 wcwidth==0.2.14