Skip to content

Commit 2052d70

Browse files
committed
v0.76 - premium mode auto-switching
1 parent 14d67bf commit 2052d70

File tree

6 files changed

+350
-3
lines changed

6 files changed

+350
-3
lines changed

.catgitinclude

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# to include in `catgit` (see https://github.com/FlyingFathead/catgit for more)
2+
src/db_utils.py
3+
src/bot_token.py
4+
src/config_paths.py
5+
src/custom_functions.py
6+
src/main.py
7+
src/modules.py
8+
src/text_message_handler.py
9+
src/utils.py
10+
config/config.ini

config/config.ini

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ AllowBotTokenFallback = True
1414
AskForTokenIfNotFound = True
1515

1616
# Model to use via OpenAI API
17+
# NOTE: SEE ALSO THE NEW AUTO-SWITCHING FEATURE UNDER: [ModelAutoSwitch]
1718
Model = gpt-4o-mini
1819

1920
# Model temperature; OpenAI's default is 0.7
@@ -114,6 +115,38 @@ ResetCommandEnabled = True
114115
# Note: needs the admin userid to be set to work!
115116
AdminOnlyReset = False
116117

118+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
119+
# Model Auto-Switching Configuration
120+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121+
[ModelAutoSwitch]
122+
# Enable automatic switching between Premium and Fallback models based on daily token limits
123+
# Set to False to always use the model specified in [DEFAULT] section's 'Model' setting.
124+
Enabled = True
125+
126+
# The preferred, more capable model to use by default (e.g., gpt-4o, gpt-4.5-preview).
127+
# This model will be used until its daily token limit (PremiumTokenLimit) is reached.
128+
PremiumModel = gpt-4o
129+
130+
# The cheaper model to switch to when the PremiumTokenLimit is reached (e.g., gpt-4o-mini).
131+
# This model has its own daily token limit (MiniTokenLimit).
132+
FallbackModel = gpt-4o-mini
133+
134+
# Daily token limit for models considered "Premium" (e.g., gpt-4o).
135+
# Set to number of tokens (i.e. 1000000 for 1M; 500000 for 500k etc)
136+
PremiumTokenLimit = 500000
137+
138+
# Daily token limit for models considered "Mini" / Fallback (e.g., gpt-4o-mini).
139+
# Corresponds to OpenAI's free tier limit for these models (typically 10,000,000).
140+
MiniTokenLimit = 10000000
141+
142+
# Action to take if the FallbackModel is selected (due to Premium limit being hit)
143+
# BUT its MiniTokenLimit is ALSO reached.
144+
# Options:
145+
# Deny - Stop processing, send a 'limit reached' message to the user. (Safest for cost)
146+
# Warn - Log a warning, proceed with the FallbackModel (will incur OpenAI costs).
147+
# Proceed - Silently proceed with the FallbackModel (will incur OpenAI costs).
148+
FallbackLimitAction = Deny
149+
117150
# ~~~~~~~~~~~~~~~~~~~
118151
# DuckDuckGo searches
119152
# ~~~~~~~~~~~~~~~~~~~

src/bot_commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ async def start(update: Update, context: CallbackContext, start_command_response
301301
# /about
302302
async def about_command(update: Update, context: CallbackContext, version_number):
303303
about_text = f"""
304+
🤖 TelegramBot-OpenAI-API ⚡️ Powered by ChatKeke 🚀
304305
This is an OpenAI-powered Telegram chatbot created by FlyingFathead.
305306
Version: v{version_number}
306307
For more information, visit: https://github.com/FlyingFathead/TelegramBot-OpenAI-API

src/db_utils.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# db_utils.py
2+
3+
import sqlite3
4+
import logging
5+
import os
6+
import time
7+
from pathlib import Path
8+
from datetime import datetime, timedelta
9+
# --- Add import for timedelta ---
10+
from datetime import timedelta
11+
12+
# --- Define DB_PATH by importing LOGS_DIR ---
13+
DB_PATH = None
14+
try:
15+
from config_paths import LOGS_DIR
16+
if LOGS_DIR and isinstance(LOGS_DIR, Path):
17+
DB_FILENAME = 'usage_tracker.db'
18+
DB_PATH = LOGS_DIR / DB_FILENAME
19+
try:
20+
LOGS_DIR.mkdir(parents=True, exist_ok=True)
21+
logging.info(f"SQLite database path set to: {DB_PATH}")
22+
except OSError as e:
23+
logging.error(f"Failed to ensure logs directory exists at {LOGS_DIR}: {e}")
24+
DB_PATH = None
25+
else:
26+
logging.critical("LOGS_DIR imported from config_paths is invalid.")
27+
DB_PATH = None
28+
except ImportError:
29+
logging.critical("Could not import LOGS_DIR from config_paths.py.")
30+
DB_PATH = None
31+
except Exception as e:
32+
logging.critical(f"Unexpected error setting DB_PATH: {e}")
33+
DB_PATH = None
34+
# --- End DB_PATH Definition ---
35+
36+
def _create_db_if_not_exists(db_path):
37+
if not db_path: return False
38+
conn = None
39+
try:
40+
conn = sqlite3.connect(db_path, timeout=10)
41+
cursor = conn.cursor()
42+
cursor.execute("""
43+
CREATE TABLE IF NOT EXISTS daily_usage (
44+
usage_date TEXT PRIMARY KEY,
45+
premium_tokens INTEGER DEFAULT 0,
46+
mini_tokens INTEGER DEFAULT 0
47+
);
48+
""")
49+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_usage_date ON daily_usage (usage_date);")
50+
conn.commit()
51+
logging.info(f"Ensured SQLite table 'daily_usage' exists in {db_path}")
52+
return True
53+
except Exception as e:
54+
logging.error(f"Failed to create or verify SQLite table in {db_path}: {e}")
55+
return False
56+
finally:
57+
if conn: conn.close()
58+
59+
DB_INITIALIZED_SUCCESSFULLY = False
60+
if DB_PATH:
61+
DB_INITIALIZED_SUCCESSFULLY = _create_db_if_not_exists(DB_PATH)
62+
else:
63+
logging.error("Database path could not be determined. SQLite functions will be disabled.")
64+
65+
def _get_daily_usage_sync(db_path, usage_date_str):
66+
"""
67+
Retrieves the token usage for a specific date.
68+
Returns tuple (premium_tokens, mini_tokens) on success,
69+
or None if DB is not initialized or any error occurs.
70+
"""
71+
if not DB_INITIALIZED_SUCCESSFULLY:
72+
logging.warning("SQLite DB not initialized. Cannot get usage.")
73+
return None # Indicate failure clearly
74+
conn = None
75+
try:
76+
conn = sqlite3.connect(db_path, timeout=10)
77+
cursor = conn.cursor()
78+
cursor.execute("SELECT premium_tokens, mini_tokens FROM daily_usage WHERE usage_date = ?", (usage_date_str,))
79+
row = cursor.fetchone()
80+
if row:
81+
premium = row[0] if row[0] is not None else 0
82+
mini = row[1] if row[1] is not None else 0
83+
return premium, mini
84+
else:
85+
# No record found for today, which is valid, return 0,0
86+
return 0, 0
87+
except sqlite3.OperationalError as e:
88+
logging.error(f"SQLite DB locked or error during read for date {usage_date_str}: {e}")
89+
return None # Indicate failure
90+
except Exception as e:
91+
logging.error(f"Error getting daily usage from SQLite ({db_path}) for date {usage_date_str}: {e}")
92+
return None # Indicate failure
93+
finally:
94+
if conn:
95+
conn.close()
96+
97+
def _update_daily_usage_sync(db_path, usage_date_str, model_tier, tokens_used):
98+
"""
99+
Adds tokens used to the appropriate counter for the given date.
100+
Handles concurrent access with retries. Does nothing if DB isn't initialized.
101+
"""
102+
if not DB_INITIALIZED_SUCCESSFULLY:
103+
logging.warning("SQLite DB not initialized. Cannot update usage.")
104+
return
105+
if tokens_used <= 0: return
106+
107+
conn = None
108+
retries = 5
109+
delay = 0.2
110+
111+
for attempt in range(retries):
112+
try:
113+
conn = sqlite3.connect(db_path, timeout=10)
114+
cursor = conn.cursor()
115+
cursor.execute("INSERT OR IGNORE INTO daily_usage (usage_date, premium_tokens, mini_tokens) VALUES (?, 0, 0)", (usage_date_str,))
116+
117+
if model_tier == 'premium':
118+
column_to_update = 'premium_tokens'
119+
elif model_tier == 'mini':
120+
column_to_update = 'mini_tokens'
121+
else:
122+
logging.warning(f"Unknown model tier '{model_tier}' provided for usage update. Cannot log.")
123+
return
124+
125+
sql = f"UPDATE daily_usage SET {column_to_update} = {column_to_update} + ? WHERE usage_date = ?"
126+
cursor.execute(sql, (tokens_used, usage_date_str))
127+
conn.commit()
128+
return # Success
129+
130+
except sqlite3.OperationalError as e:
131+
if "locked" in str(e).lower() and attempt < retries - 1:
132+
logging.warning(f"SQLite DB locked during write (Attempt {attempt+1}/{retries}). Retrying in {delay:.2f}s...")
133+
time.sleep(delay)
134+
delay = min(delay * 1.5, 2.0)
135+
else:
136+
logging.error(f"SQLite DB locked or error during write for {usage_date_str} after {retries} attempts: {e}")
137+
if conn: conn.rollback()
138+
break
139+
except Exception as e:
140+
logging.error(f"Unexpected error updating daily usage in SQLite ({db_path}) for {usage_date_str}: {e}")
141+
if conn: conn.rollback()
142+
break
143+
finally:
144+
if conn:
145+
conn.close()
146+
logging.error(f"Failed to update usage for {usage_date_str}, tier {model_tier}, tokens {tokens_used} after {retries} attempts.")
147+
148+
149+
def _cleanup_old_usage_sync(db_path, max_history_days):
150+
"""Deletes usage records older than max_history_days."""
151+
if not DB_INITIALIZED_SUCCESSFULLY:
152+
logging.warning("SQLite DB not initialized. Cannot cleanup old usage.")
153+
return
154+
conn = None
155+
try:
156+
conn = sqlite3.connect(db_path, timeout=10)
157+
cursor = conn.cursor()
158+
cutoff_date_dt = datetime.utcnow() - timedelta(days=max_history_days)
159+
cutoff_date_str = cutoff_date_dt.strftime('%Y-%m-%d')
160+
cursor.execute("DELETE FROM daily_usage WHERE usage_date < ?", (cutoff_date_str,))
161+
deleted_count = cursor.rowcount
162+
conn.commit()
163+
if deleted_count > 0:
164+
logging.info(f"SQLite Cleanup: Deleted {deleted_count} usage records older than {cutoff_date_str}.")
165+
except sqlite3.OperationalError as e:
166+
logging.error(f"SQLite DB locked or error during cleanup: {e}")
167+
except Exception as e:
168+
logging.error(f"Error cleaning up old usage in SQLite ({db_path}): {e}")
169+
finally:
170+
if conn:
171+
conn.close()

src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
99
#
1010
# version of this program
11-
version_number = "0.75056"
11+
version_number = "0.76"
1212

1313
# Add the project root directory to Python's path
1414
import sys

0 commit comments

Comments
 (0)