привет! вот код моего телеграм бота, ознакомься. в следующем сообщении дам config.json для него и в нём же скажу что надо сделать с кодом бота.
import os
import json
import logging
import re
import html
import asyncio
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from io import BytesIO
from PIL import Image
import aiohttp
import openai
from telegram import (
    Update,
    InlineKeyboardMarkup,
    InlineKeyboardButton,
    InputFile,
    ReplyKeyboardMarkup
)
from telegram.constants import ParseMode
from telegram.error import TelegramError
from telegram.ext import (
    ApplicationBuilder,
    CommandHandler,
    MessageHandler,
    ContextTypes,
    filters,
    CallbackQueryHandler,
    Defaults,
)

# Настройка логирования
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO
)
logger = logging.getLogger(__name__)

# Определение директории скрипта
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

# Утилиты для работы с JSON
def load_json(file_name, default=None):
    file_path = os.path.join(SCRIPT_DIR, file_name)
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except FileNotFoundError:
        if default is not None:
            return default
        logger.error(f"Файл {file_name} не найден по пути: {file_path}.")
        exit(1)
    except json.JSONDecodeError as e:
        logger.error(f"Ошибка разбора {file_name}: {e}")
        exit(1)
    except Exception as e:
        logger.error(f"Ошибка при загрузке {file_name}: {e}")
        exit(1)

def save_json(file_name, data):
    file_path = os.path.join(SCRIPT_DIR, file_name)
    try:
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"Ошибка при сохранении {file_name}: {e}")

# Загрузка конфигурационных файлов
config = load_json('config.json', default={
    "limits": {
        "cablyai": {"daily_token_limit": 2000000, "image_daily_limit": 10000, "admin_exempt_threshold": 100000},
        "helix": {"daily_requests": 50, "per_user_daily_requests": 15},
        "meowai": {"total_requests": 960, "per_user_requests": 50},
    },
    "authorized_user_ids": [],
    "models": {},
    "providers": {},
    "system_instruction": "ты ведёшь себя как хороший ассистент, готовый ответить на любой вопрос."
})
usage_store = load_json('usage.json', default={
    "cablyai_tokens": 0,
    "helix_requests": 0,
    "meowai_requests": 0,
    "users": {}
})
user_mode_store = load_json('user_modes.json', default={})

# Настройка глобальных переменных
AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))
model_id_to_info = {info['id']: info for name, info in config.get('models', {}).items() if 'id' in info}
model_id_to_name = {model_id: name.lower() for name, info in config.get('models', {}).items() if (model_id := info.get('id'))}
RESTRICTED_MODELS = ['o1-preview', 'o1-mini', 'openai-o1-preview', 'openai-o1-mini']
AUTHORIZED_MODEL_USERS = {5005816138, 1759650343, 7856892361, 488179251, 1110920711}
SYSTEM_INSTRUCTION_TEXT = config.get('system_instruction', "ты ведёшь себя как хороший ассистент, готовый ответить на любой вопрос.")
MAX_MESSAGE_LENGTH = 4096
MAX_IMAGE_SIZE = 50 * 1024 * 1024  # 50MB

# Структуры для хранения состояния
context_store = {}
user_state = {}
current_provider_store = {}  # Новая структура для хранения текущего провайдера пользователя

# Создание необходимых директорий
os.makedirs(os.path.join(SCRIPT_DIR, 'chats'), exist_ok=True)
os.makedirs(os.path.join(SCRIPT_DIR, 'downloads', 'photos'), exist_ok=True)
os.makedirs(os.path.join(SCRIPT_DIR, 'downloads', 'files'), exist_ok=True)
os.makedirs(os.path.join(SCRIPT_DIR, 'responses'), exist_ok=True)

# Отображение названий провайдеров
provider_display_names = {
    'cablyai': 'CatAI',
    'openai': 'OpenAI',
    'helix': 'Helix',
    'meowai': 'MeowAI',
}

# Функции для работы с чатами и лимитами
def load_chat_context(user_id, model_id, chat_slot, system_instruction):
    file_path = os.path.join(SCRIPT_DIR, 'chats', f"{user_id}_{model_id}_chat{chat_slot}.json")
    if not os.path.exists(file_path):
        return [{"role": "system", "content": system_instruction}]
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception as e:
        logger.error(f"Ошибка загрузки {file_path}: {e}")
        return [{"role": "system", "content": system_instruction}]

def save_chat_context(user_id, model_id, chat_slot, context):
    try:
        with open(os.path.join(SCRIPT_DIR, 'chats', f"{user_id}_{model_id}_chat{chat_slot}.json"), 'w', encoding='utf-8') as f:
            json.dump(context, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"Ошибка сохранения чата: {e}")

def check_and_increment_limit(user_id, provider):
    """
    Проверяет и увеличивает счетчики лимитов для провайдера и пользователя.
    Возвращает True, если разрешено продолжить, иначе False.
    """
    provider = provider.lower()
    if provider not in config['limits']:
        # Нет лимитов для этого провайдера
        return True

    # Получаем лимиты
    if provider == 'cablyai':
        limit = config['limits']['cablyai']['daily_token_limit']
        admin_exempt_threshold = config['limits']['cablyai']['admin_exempt_threshold']
        used = usage_store.get('cablyai_tokens', 0)
        if user_id not in AUTHORIZED_MODEL_USERS and used >= limit - admin_exempt_threshold:
            # Доступ закрыт для всех, кроме админа
            return False
    elif provider == 'helix':
        limit = config['limits']['helix']['daily_requests']
        used = usage_store.get('helix_requests', 0)
        if used >= limit:
            return False
    elif provider == 'meowai':
        limit = config['limits']['meowai']['total_requests']
        used = usage_store.get('meowai_requests', 0)
        if used >= limit:
            return False
    else:
        return True  # Для других провайдеров лимиты не изменяются

    # Получаем параметры пользователя
    user_limits = config.get('user_limits', {}).get(str(user_id), {})
    provider_user_limit = user_limits.get(provider, "infinite")
    if provider_user_limit != "infinite":
        if provider == 'cablyai':
            user_used = usage_store.get('users', {}).get(str(user_id), {}).get('cablyai_tokens', 0)
            if user_used >= provider_user_limit:
                return False
        elif provider == 'helix':
            user_used = usage_store.get('users', {}).get(str(user_id), {}).get('helix_requests', 0)
            if user_used >= provider_user_limit:
                return False
        elif provider == 'meowai':
            user_used = usage_store.get('users', {}).get(str(user_id), {}).get('meowai_requests', 0)
            if user_used >= provider_user_limit:
                return False

    # Увеличиваем глобальные лимиты
    if provider == 'cablyai':
        usage_store['cablyai_tokens'] = usage_store.get('cablyai_tokens', 0) + 0  # Учет токенов происходит после запроса
    elif provider == 'helix':
        usage_store['helix_requests'] = usage_store.get('helix_requests', 0) + 1
    elif provider == 'meowai':
        usage_store['meowai_requests'] = usage_store.get('meowai_requests', 0) + 1

    # Увеличиваем пользовательские лимиты
    if provider_user_limit != "infinite":
        if provider == 'cablyai':
            usage_store.setdefault('users', {}).setdefault(str(user_id), {}).setdefault('cablyai_tokens', 0)
        elif provider == 'helix':
            usage_store.setdefault('users', {}).setdefault(str(user_id), {}).setdefault('helix_requests', 0)
        elif provider == 'meowai':
            usage_store.setdefault('users', {}).setdefault(str(user_id), {}).setdefault('meowai_requests', 0)

    save_json('usage.json', usage_store)
    return True

# Вспомогательные функции
def markdown_to_html(text):
    replacements = {
        r'\*\*(.*?)\*\*': r'<b>\1</b>',
        r'\*(.*?)\*': r'<i>\1</i>',
        r'__(.*?)__': r'<u>\1</u>',
        r'~~(.*?)~~': r'
\1
',
        r'#{1,6} (.*?)\n': r'<b>\1</b>\n'
    }
    text = html.escape(text)
    for pattern, repl in replacements.items():
        text = re.sub(pattern, repl, text, flags=re.MULTILINE)
    return text

def split_message(message, max_length):
    return [message[i:i + max_length] for i in range(0, len(message), max_length)]

def get_provider_buttons():
    keyboard = [['CatAI', 'OpenAI'], ['Helix', 'MeowAI']]
    return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)

def get_model_buttons_by_provider(provider):
    buttons = [
        [InlineKeyboardButton(html.escape(name), callback_data=f"model_select:{info['id']}")]
        for name, info in config.get('models', {}).items()
        if info.get('provider', 'cablyai').lower() == provider.lower()
    ]
    # Добавляем кнопку "сменить провайдера" в конце
    buttons.append([InlineKeyboardButton("сменить провайдера", callback_data='change_provider')])
    return InlineKeyboardMarkup(buttons)

def get_chat_slot_buttons(model_id):
    return InlineKeyboardMarkup([
        [InlineKeyboardButton("Чат 1", callback_data=f"chat_slot:1:{model_id}"),
         InlineKeyboardButton("Чат 2", callback_data=f"chat_slot:2:{model_id}")]
    ])

def get_main_buttons(user_id, model_id=None):
    buttons = []
    if model_id:
        buttons += [
            [InlineKeyboardButton("сменить модель", callback_data='change_model'),
             InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
             InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
            [InlineKeyboardButton("продолжить", callback_data='continue')]
        ]
    else:
        buttons += [[InlineKeyboardButton("сменить модель", callback_data='change_model')]]
    return InlineKeyboardMarkup(buttons)

def save_md_file(content, user_id):
    filename = os.path.join(SCRIPT_DIR, 'responses', f"{user_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.md")
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(content)
        return filename
    except Exception as e:
        logger.error(f"Ошибка сохранения .md файла: {e}")
        return None

def is_image_supported(model_id):
    supported_models = [
        'openai-chatgpt-4o-latest', 'openai-gpt-4-turbo', 'openai-gpt-4o-mini',
        'chatgpt-4o-latest', 'gpt-4-turbo', 'gpt-4o-mini',
        "FLUX.1.1 [pro]", "flux.1.1-pro-ultra", "FLUX 1.1 [pro] ultra raw",
        "sd3.5", "stable-diffusion-3.5-turbo"
    ]
    return model_id in supported_models

def get_api_model_id(model_id, provider):
    prefixes = {'openai': 'openai-', 'helix': 'helix-', 'meowai': 'meowai-'}
    prefix = prefixes.get(provider, '')
    return model_id[len(prefix):] if model_id.startswith(prefix) else model_id

# Асинхронные функции для API запросов
async def send_api_request(provider, model_id, messages=None, prompt=None, model_type='text'):
    provider_info = config['providers'].get(provider, {})
    if model_type in ['text', 'multimodal']:
        api_url = provider_info.get('api_endpoint_text') or provider_info.get('api_endpoint')
    elif model_type == 'image':
        api_url = provider_info.get('api_endpoint_image') or provider_info.get('api_endpoint')
    else:
        logger.error(f"Неизвестный тип модели: {model_type}")
        return None

    if not api_url or not provider_info.get('api_key'):
        logger.error(f"{provider} API настройки отсутствуют в config.json.")
        return None

    headers = {'Authorization': f"Bearer {provider_info['api_key']}", 'Content-Type': 'application/json'}
    data = {"model": get_api_model_id(model_id, provider)}

    if model_type in ['text', 'multimodal']:
        data["messages"] = messages
    elif model_type == 'image':
        data.update({"prompt": prompt, "n": 1, "size": "1024x1024"})
    else:
        logger.error(f"Неизвестный тип модели: {model_type}")
        return None

    logger.info(f"Отправка запроса к {provider} API на URL: {api_url} с данными: {data}")

    try:
        async with aiohttp.ClientSession() as session:
            async with session.post(api_url, headers=headers, json=data) as response:
                if response.status in [200, 201]:
                    response_json = await response.json()
                    logger.info(f"Получен ответ от {provider} API: {response_json}")
                    return response_json
                else:
                    error_text = await response.text()
                    logger.error(f"Ошибка запроса к {provider} API: {response.status} - {error_text}")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при запросе к {provider} API: {e}")
        return None

async def translate_to_english(text):
    translation_messages = [
        {"role": "system", "content": "you are a helpful assistant that translates russian text to english."},
        {"role": "user", "content": f"переведи следующий текст на английский язык и верни только переведенный текст:\n\n{text}"}
    ]
    response = await send_api_request('cablyai', "chatgpt-4o-latest", messages=translation_messages, model_type='multimodal')
    if response and "choices" in response and response["choices"]:
        # Обработка использования токенов для CablyAI
        if 'usage' in response and 'total_tokens' in response['usage']:
            total_tokens = response['usage']['total_tokens']
            limit = config['limits'].get('cablyai', {}).get('daily_token_limit', 2000000)
            used = usage_store.get('cablyai_tokens', 0)
            if used + total_tokens > limit:
                logger.info("CablyAI daily token limit exceeded during translation.")
                return None
            usage_store['cablyai_tokens'] = used + total_tokens
            # Увеличиваем персональный лимит пользователя
            usage_store.setdefault('users', {}).setdefault(str(user_id), {}).setdefault('cablyai_tokens', 0)
            user_limit = config.get('user_limits', {}).get(str(user_id), {}).get('cablyai', "infinite")
            if user_limit != "infinite":
                usage_store['users'][str(user_id)]['cablyai_tokens'] += total_tokens
            save_json('usage.json', usage_store)
        return response["choices"][0].get('message', {}).get('content', '').strip()
    logger.error("Ошибка при переводе текста.")
    return None

async def generate_image(provider, model_id, prompt):
    # Оповещаем пользователя о задержке
    await context.bot.send_message(
        chat_id=user_id,
        text="Картинка будет генерироваться чуть дольше из-за необходимости перевода текста на английский."
    )
    response = await send_api_request(provider, model_id, prompt=prompt, model_type='image')
    if response and 'data' in response and response['data']:
        return response['data'][0].get('url')
    logger.error(f"Ошибка генерации изображения через {provider} API.")
    return None

# Функция для получения ответа модели
async def get_model_response(provider, model_id, messages, model_type, user_id, prompt=None):
    if provider in ['helix', 'meowai']:
        response = await send_api_request(provider, model_id, messages=messages, model_type=model_type)
        if response and isinstance(response, dict):
            if 'choices' in response and response['choices']:
                return response['choices'][0].get('message', {}).get('content', None)
    elif provider == 'cablyai':
        response = await send_api_request(provider, model_id, messages=messages, prompt=prompt, model_type=model_type)
        if model_type == 'image':
            return response.get('data', [{}])[0].get('url', None)
        elif response and "choices" in response and response["choices"]:
            # Обработка использования токенов для CablyAI
            if 'usage' in response and 'total_tokens' in response['usage']:
                total_tokens = response['usage']['total_tokens']
                limit = config['limits'].get('cablyai', {}).get('daily_token_limit', 2000000)
                used = usage_store.get('cablyai_tokens', 0)
                if used + total_tokens > limit:
                    logger.info("CablyAI daily token limit exceeded.")
                    return None
                usage_store['cablyai_tokens'] = used + total_tokens
                # Увеличиваем персональный лимит пользователя
                usage_store.setdefault('users', {}).setdefault(str(user_id), {}).setdefault('cablyai_tokens', 0)
                user_limit = config.get('user_limits', {}).get(str(user_id), {}).get('cablyai', "infinite")
                if user_limit != "infinite":
                    usage_store['users'][str(user_id)]['cablyai_tokens'] += total_tokens
                save_json('usage.json', usage_store)
            return response["choices"][0].get('message', {}).get('content', None)
    elif provider == 'openai':
        api_model = get_api_model_id(model_id, provider)
        try:
            if model_type == 'image' and prompt:
                # Предполагается, что OpenAI обрабатывает образы отдельно
                response = openai.Image.create_edit(image=open(prompt, 'rb'), prompt=prompt, n=1, size="1024x1024")
                return response['data'][0].get('url', None)
            else:
                response = openai.ChatCompletion.create(model=api_model, messages=messages)
                return response['choices'][0]['message']['content']
        except openai.error.OpenAIError as e:
            logger.error(f"Ошибка OpenAI API: {e}")
    return None

# Функция handle_continue
async def handle_continue(user_id, context_obj, update, query):
    current_model_id = user_state[user_id].get('current_model_id')
    current_chat_slot = user_state[user_id].get('current_chat_slot')
    if not current_model_id or not current_chat_slot:
        await query.answer(text='Модель или чат не выбраны.', show_alert=True)
        return
    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai').lower()
    model_type = model_info.get('type', 'text').lower()
    # Проверка доступности модели
    if provider == 'cablyai':
        global_tokens = usage_store.get('cablyai_tokens', 0)
        admin_exempt_threshold = config['limits']['cablyai'].get('admin_exempt_threshold', 100000)
        if global_tokens >= config['limits']['cablyai']['daily_token_limit'] - admin_exempt_threshold:
            if user_id not in AUTHORIZED_MODEL_USERS:
                await query.answer(text='Доступ к CatAI моделям временно закрыт.', show_alert=True)
                return
    elif provider in ['helix', 'meowai']:
        if not check_and_increment_limit(user_id, provider):
            await query.answer(text='Вы достигли лимита запросов для этого провайдера.', show_alert=True)
            return
    context_messages = context_store[user_id][current_model_id][current_chat_slot]
    context_messages.append({"role": "user", "content": "Продолжи"})
    answer = await get_model_response(provider, current_model_id, context_messages, model_type, user_id)
    if answer:
        context_messages.append({"role": "assistant", "content": answer})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_messages)
        if model_type != 'image':
            answer_html = markdown_to_html(answer)
            for msg in split_message(answer_html, MAX_MESSAGE_LENGTH):
                await update.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
    else:
        await query.answer(text='Не удалось получить ответ от модели или превышен лимит.', show_alert=True)

# Обработчики команд
async def add_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
    admin_id = 5005816138
    if update.effective_user.id != admin_id:
        await update.message.reply_text("У вас нет прав для использования этой команды.")
        return
    if len(context.args) != 1 or not context.args[0].isdigit():
        await update.message.reply_text("Использование: /adduser <telegram_id>")
        return
    new_user_id = int(context.args[0])
    if new_user_id in AUTHORIZED_USER_IDS:
        await update.message.reply_text("Этот пользователь уже авторизован.")
        return
    AUTHORIZED_USER_IDS.add(new_user_id)
    # Установка персональных лимитов по умолчанию (можно настроить отдельно)
    config.setdefault('user_limits', {})[str(new_user_id)] = {
        "cablyai": 500000,
        "helix": 15,
        "meowai": 50
    }
    save_json('config.json', config)
    await update.message.reply_text(f"Пользователь {new_user_id} успешно добавлен.")
    try:
        await context.bot.send_message(chat_id=new_user_id, text="Вас добавили в список пользователей, имеющих доступ к боту.")
    except Exception as e:
        logger.error(f"Не удалось отправить сообщение новому пользователю {new_user_id}: {e}")

# Описания моделей
MODEL_DESCRIPTIONS = {
    "claude-3.5-sonnet-20241022": "передовая модель Anthropic. быстрая, в задачах с кодом в чём-то лучше даже OpenAI o1-mini и preview. актуальность знаний: до апреля 2024. контекстное окно: 200000 токенов. выплюнуть за раз может: 5000 символов ( символ не равно токен ).",
    "claude-3.5-haiku": "почти передовая модель Anthropic. совсем новая, быстрая, лучше чем предшественник Claude 3 Haiku. её учили хорошо писать код! актуальность знаний: неизвестна. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "claude-3-opus": "бывшая передовая модель Anthropic. медленная, но очень хороша в генерации текстов и удивляет своими познаниями русского языка. актуальность знаний: до августа 2023. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "chatgpt-4o-latest": "новейшая версия модели GPT-4o за, вроде, сентябрь 2024, созданная OpenAI. быстрая, хороша для широкого спектра задач. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "gpt-4-turbo": "бывшая передовая модель OpenAI. иногда в чём-то превосходит даже GPT-4o-latest, но уступает в скорости и сильно дороже. актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 4096 токенов.",
    "gpt-4o-mini": "уменьшенная версия GPT-4o. очень быстрая! но по качеству ответов уступает. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "o1-preview": "передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер, но генерирует ответ довольно долго относительно других моделей. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 65536 токенов.",
    "o1-mini": "уменьшенная версия модели o1 от OpenAI. тоже использует цепочку рассуждений, но думает намного быстрее. может уступать o1-preview в качестве ответов, но это компенсируется её на 80% более низкой ценой (для тех кто платит за неё) и быстротой ответов. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов.",
    "llama-3.1-405b-turbo": "текстовая модель. сопоставима по качеству ответов с остальными моделями, но может уступать, и выдавать всякие иероглифы посреди ответа. но почему бы и не добавить? актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: неизвестно.",
    "FLUX.1.1 [pro]": "лучшая существующая модель генерации изображений. некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "flux.1.1-pro-ultra": "как Flux 1.1 Pro, но с х4 большим качеством картинок! некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "FLUX 1.1 [pro] ultra raw": "как Flux 1.1 Pro, но с х4 большим качеством картинок! сделана так чтобы в картинках было меньше синтетики и больше реальности. некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "sd3.5": "модель для генерации изображений по текстовому описанию. контекста не имеет, работая лишь с текущим заданным запросом.",
    "stable-diffusion-3.5-turbo": "более новая и быстрая модель для генерации изображений по текстовому описанию. контекста не имеет, работая лишь с текущим заданным запросом.",
    "helix-chatgpt-4o-latest": "та же chatgpt-4o-latest, что и выше. отличия: поставляется с помощью ключа api Helix.",
    "meowai-chatgpt-4o-latest": "та же chatgpt-4o-latest, что и выше. отличия: поставляется с помощью ключа api MeowAI.",
    "openai-chatgpt-4o-latest": "та же chatgpt-4o-latest, что и выше. отличия: поставляется с помощью ключа api OpenAI.",
    "openai-gpt-4-turbo": "та же GPT-4 Turbo, что и выше. отличия: поставляется с помощью ключа api OpenAI.",
    "openai-gpt-4o-mini": "та же GPT-4o Mini, что и выше. отличия: поставляется с помощью ключа api OpenAI.",
    "openai-o1-preview": "та же o1-preview, что и выше. отличия: поставляется с помощью ключа api OpenAI.",
    "openai-o1-mini": "та же o1-mini, что и выше. отличия: поставляется с помощью ключа api OpenAI."
}

async def models_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if not model_id_to_info:
        await update.message.reply_text('Нет доступных моделей.', reply_markup=get_main_buttons(user_id))
        return

    descriptions = []
    for mid in model_id_to_info:
        model_name = model_id_to_name.get(mid, 'Неизвестная модель').capitalize()
        description = MODEL_DESCRIPTIONS.get(mid, 'Описание для этой модели отсутствует.')
        descriptions.append(f"- <b>{html.escape(model_name)}</b>: {html.escape(description)}")

    chunk_size = 10
    for i in range(0, len(descriptions), chunk_size):
        chunk = descriptions[i:i+chunk_size]
        message = "<b>Описание доступных моделей:</b>\n\n" + "\n".join(chunk)
        await update.message.reply_text(message, parse_mode=ParseMode.HTML)

async def setmode(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return
    if len(context.args) != 1 or context.args[0].lower() not in ['message', 'file']:
        await update.message.reply_text('Использование: /setmode [message|file]')
        return
    mode = context.args[0].lower()
    user_mode_store[str(user_id)] = mode
    save_json('user_modes.json', user_mode_store)
    await update.message.reply_text(f'Режим получения ответов установлен на: <b>{mode}</b>', parse_mode=ParseMode.HTML)

async def mode_command_func(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return
    mode = user_mode_store.get(str(user_id), 'message')
    await update.message.reply_text(f'Текущий режим получения ответов: <b>{mode}</b>', parse_mode=ParseMode.HTML)

async def limits_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("у тебя нет доступа к этому боту.")
        return
    user_limits = config.get('user_limits', {}).get(str(user_id), {})
    global_limits = config.get('limits', {})
    lines = ["<b>Ваши лимиты:</b>"]

    # CatAI
    cablyai_used = usage_store.get('cablyai_tokens', 0)
    cablyai_limit = global_limits.get('cablyai', {}).get('daily_token_limit', 2000000)
    cablyai_remaining = max(cablyai_limit - cablyai_used, 0)
    cablyai_user_limit = user_limits.get('cablyai', "infinite")
    cablyai_user_used = usage_store.get('users', {}).get(str(user_id), {}).get('cablyai_tokens', 0)
    if cablyai_user_limit == "infinite":
        user_limit_display = "∞"
        remaining_user = "∞"
    else:
        user_limit_display = f"{cablyai_user_limit}"
        remaining_user = f"{max(cablyai_user_limit - cablyai_user_used, 0)}"
    lines.append(f"\n- <b>CatAI (CablyAI)</b>: Использовано токенов: {cablyai_used} / {cablyai_limit} (Осталось: {cablyai_remaining})")
    lines.append(f"  Ваш лимит: {cablyai_user_used} / {user_limit_display} (Осталось: {remaining_user})")

    # Helix
    helix_used = usage_store.get('helix_requests', 0)
    helix_limit = global_limits.get('helix', {}).get('daily_requests', 50)
    helix_remaining = max(helix_limit - helix_used, 0)
    helix_user_limit = user_limits.get('helix', "infinite")
    helix_user_used = usage_store.get('users', {}).get(str(user_id), {}).get('helix_requests', 0)
    if helix_user_limit == "infinite":
        helix_user_display = "∞"
        helix_remaining_user = "∞"
    else:
        helix_user_display = f"{helix_user_limit}"
        helix_remaining_user = f"{max(helix_user_limit - helix_user_used, 0)}"
    lines.append(f"\n- <b>Helix</b>: Использовано запросов: {helix_used} / {helix_limit} (Осталось: {helix_remaining})")
    lines.append(f"  Ваш лимит: {helix_user_used} / {helix_user_display} (Осталось: {helix_remaining_user})")

    # MeowAI
    meowai_used = usage_store.get('meowai_requests', 0)
    meowai_limit = global_limits.get('meowai', {}).get('total_requests', 960)
    meowai_remaining = max(meowai_limit - meowai_used, 0)
    meowai_user_limit = user_limits.get('meowai', "infinite")
    meowai_user_used = usage_store.get('users', {}).get(str(user_id), {}).get('meowai_requests', 0)
    if meowai_user_limit == "infinite":
        meowai_user_display = "∞"
        meowai_remaining_user = "∞"
    else:
        meowai_user_display = f"{meowai_user_limit}"
        meowai_remaining_user = f"{max(meowai_user_limit - meowai_user_used, 0)}"
    lines.append(f"\n- <b>MeowAI</b>: Использовано запросов: {meowai_used} / {meowai_limit} (Осталось: {meowai_remaining})")
    lines.append(f"  Ваш лимит: {meowai_user_used} / {meowai_user_display} (Осталось: {meowai_remaining_user})")

    reset_time = config.get('limit_reset_time', '02:00')
    lines.append(f"\n<code>Лимиты сбросятся в {reset_time} по московскому времени.</code>")
    await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.HTML)

async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return
    user_state[user_id] = {'current_model_id': None, 'current_chat_slot': None, 'current_provider': None}
    current_provider_store[user_id] = None  # Инициализация текущего провайдера
    await update.message.reply_text('привет! выбери провайдера моделей:', reply_markup=get_provider_buttons())

async def reload_bot(update: Update, context: ContextTypes.DEFAULT_TYPE):
    admin_ids = {5005816138}
    user_id = update.effective_user.id
    if user_id not in admin_ids:
        await update.message.reply_text('ты не админ и тебе такое нельзя.')
        return
    await update.message.reply_text('перезагружаю конфигурацию бота...')
    try:
        global config, model_id_to_info, model_id_to_name, AUTHORIZED_USER_IDS, RESTRICTED_MODELS, AUTHORIZED_MODEL_USERS, SYSTEM_INSTRUCTION_TEXT, usage_store, user_mode_store
        config = load_json('config.json', default=config)
        AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))
        model_id_to_info = {info['id']: info for name, info in config.get('models', {}).items() if 'id' in info}
        model_id_to_name = {model_id: name.lower() for name, info in config.get('models', {}).items() if (model_id := info.get('id'))}
        RESTRICTED_MODELS = ['o1-preview', 'o1-mini', 'openai-o1-preview', 'openai-o1-mini']
        AUTHORIZED_MODEL_USERS = {5005816138, 1759650343, 7856892361, 488179251}  # Обновите при необходимости
        SYSTEM_INSTRUCTION_TEXT = config.get('system_instruction', "ты ведёшь себя как хороший ассистент, готовый ответить на любой вопрос.")
        usage_store = load_json('usage.json', default=usage_store)
        user_mode_store = load_json('user_modes.json', default=user_mode_store)
        save_json('config.json', config)
        save_json('usage.json', usage_store)
        save_json('user_modes.json', user_mode_store)
        await update.message.reply_text('Конфигурация успешно перезагружена.')
        logger.info("Конфигурация бота успешно перезагружена.")
    except Exception as e:
        logger.error(f"Ошибка при перезагрузке конфигурации: {e}")
        await update.message.reply_text('Произошла ошибка при перезагрузке конфигурации.')

# Обработчики CallbackQuery
async def handle_callback_query(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    user_id = query.from_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return
    data = query.data
    if data.startswith("model_select:"):
        model_id = data.split("model_select:")[1]
        model_info = model_id_to_info.get(model_id, {})
        provider = model_info.get('provider', 'cablyai').lower()
        # Проверка доступности модели
        if provider == 'cablyai':
            global_tokens = usage_store.get('cablyai_tokens', 0)
            admin_exempt_threshold = config['limits']['cablyai'].get('admin_exempt_threshold', 100000)
            if global_tokens >= config['limits']['cablyai']['daily_token_limit'] - admin_exempt_threshold:
                if user_id not in AUTHORIZED_MODEL_USERS:
                    await query.message.reply_text('Доступ к CatAI моделям временно закрыт.')
                    return
        elif provider in ['helix', 'meowai']:
            if not check_and_increment_limit(user_id, provider):
                await query.message.reply_text('Вы достигли лимита запросов для этого провайдера.', reply_markup=get_main_buttons(user_id, model_id))
                return
        user_state[user_id]['current_model_id'] = model_id
        user_state[user_id]['current_provider'] = provider
        await context.bot.send_message(
            chat_id=user_id,
            text=f'Выбрана модель: <b>{html.escape(model_id_to_name.get(model_id, "Неизвестная модель")).capitalize()}</b>\n\nВыберите слот чата:',
            parse_mode=ParseMode.HTML,
            reply_markup=get_chat_slot_buttons(model_id)
        )
    elif data.startswith("chat_slot:"):
        _, slot, model_id = data.split(":")
        slot = int(slot)
        model_info = model_id_to_info.get(model_id, {})
        provider = model_info.get('provider', 'cablyai').lower()
        user_state[user_id].update({'current_chat_slot': slot, 'current_provider': provider})
        system_instruction = SYSTEM_INSTRUCTION_TEXT
        if provider == 'openai' and model_id not in ['openai-o1-preview', 'openai-o1-mini']:
            system_instruction = "You are a helpful assistant."
        context_store.setdefault(user_id, {}).setdefault(model_id, {}).setdefault(slot, load_chat_context(user_id, model_id, slot, system_instruction))
        await context.bot.send_message(
            chat_id=user_id,
            text=f'Выбран <b>чат {slot}</b> для модели <b>{html.escape(model_id_to_name.get(model_id, "Неизвестная модель")).capitalize()}</b>',
            parse_mode=ParseMode.HTML,
            reply_markup=get_main_buttons(user_id, model_id)
        )
    elif data == 'change_model':
        current_model_id = user_state[user_id].get('current_model_id')
        current_provider = user_state[user_id].get('current_provider')
        if not current_provider:
            await query.message.reply_text('провайдер не установлен. используй /start или "сменить провайдера".')
            return
        user_state[user_id].update({'current_model_id': None, 'current_chat_slot': None})
        await context.bot.send_message(
            chat_id=user_id,
            text='выбери модель этого провайдера:',
            parse_mode=ParseMode.HTML,
            reply_markup=get_model_buttons_by_provider(current_provider)
        )
    elif data == 'change_provider':
        user_state[user_id].update({'current_model_id': None, 'current_chat_slot': None, 'current_provider': None})
        current_provider_store[user_id] = None
        await context.bot.send_message(
            chat_id=user_id,
            text='Выбери нового провайдера для взаимодействия:',
            parse_mode=ParseMode.HTML,
            reply_markup=get_provider_buttons()
        )
    elif data == 'clear_context':
        current_model_id = user_state[user_id].get('current_model_id')
        current_chat_slot = user_state[user_id].get('current_chat_slot')
        if not current_model_id or not current_chat_slot:
            await query.message.reply_text('Модель или чат не выбраны.')
            return
        model_info = model_id_to_info.get(current_model_id, {})
        provider = model_info.get('provider', 'cablyai').lower()
        # Проверка доступности модели
        if provider == 'cablyai':
            global_tokens = usage_store.get('cablyai_tokens', 0)
            admin_exempt_threshold = config['limits']['cablyai'].get('admin_exempt_threshold', 100000)
            if global_tokens >= config['limits']['cablyai']['daily_token_limit'] - admin_exempt_threshold:
                if user_id not in AUTHORIZED_MODEL_USERS:
                    await query.message.reply_text('Доступ к CatAI моделям временно закрыт.')
                    return
        elif provider in ['helix', 'meowai']:
            if not check_and_increment_limit(user_id, provider):
                await query.message.reply_text('Вы достигли лимита запросов для этого провайдера.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        system_instruction = SYSTEM_INSTRUCTION_TEXT
        if provider == 'openai' and current_model_id not in ['openai-o1-preview', 'openai-o1-mini']:
            system_instruction = "You are a helpful assistant."
        context_store[user_id][current_model_id][current_chat_slot] = [{"role": "system", "content": system_instruction}]
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])
        await context.bot.send_message(
            chat_id=user_id,
            text=f'Контекст <b>Чат {current_chat_slot}</b> для модели <b>{html.escape(model_id_to_name.get(current_model_id, "Неизвестная модель")).capitalize()}</b> очищен.',
            parse_mode=ParseMode.HTML,
            reply_markup=get_main_buttons(user_id, current_model_id)
        )
    elif data == 'continue':
        await handle_continue(user_id, context, update, query)

# Обработчик выбора провайдера через кнопки
async def handle_provider_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_text = update.message.text.lower()
    internal_provider = {'catai': 'cablyai', 'openai': 'openai', 'helix': 'helix', 'meowai': 'meowai'}.get(user_text)
    if not internal_provider:
        await update.message.reply_text("Некорректный провайдер.")
        return
    user_id = update.effective_user.id
    user_state[user_id].update({'current_provider': internal_provider, 'current_model_id': None, 'current_chat_slot': None})
    current_provider_store[user_id] = internal_provider
    await update.message.reply_text(
        f'Выбран провайдер: <b>{html.escape(provider_display_names.get(internal_provider, user_text.capitalize()))}</b>\n\nВыберите модель:',
        parse_mode=ParseMode.HTML,
        reply_markup=get_model_buttons_by_provider(internal_provider)
    )

# Обработчики сообщений
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return
    photo = update.message.photo[-1]
    file = await photo.get_file()
    file_path = os.path.join(SCRIPT_DIR, 'downloads', 'photos', f"{user_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.jpg")
    try:
        await file.download_to_drive(file_path)
    except Exception as e:
        logger.error(f"Ошибка скачивания фото: {e}")
        await update.message.reply_text('Не удалось скачать изображение.')
        return
    user_caption = update.message.caption or ""
    if user_caption:
        translated_text = await translate_to_english(user_caption)
        if translated_text:
            user_caption = translated_text
        else:
            await update.message.reply_text("Не удалось перевести текст.")
            return
    current_model_id = user_state[user_id].get('current_model_id')
    current_chat_slot = user_state[user_id].get('current_chat_slot')
    if not current_model_id or not current_chat_slot:
        await update.message.reply_text('Модель или чат не выбраны.', reply_markup=get_main_buttons(user_id))
        return
    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai').lower()
    if not is_image_supported(current_model_id):
        await update.message.reply_text('Выбранная модель не поддерживает обработку изображений.', reply_markup=get_main_buttons(user_id, current_model_id))
        return
    # Проверка лимитов для провайдеров (кроме cablyai, где лимит учитывается после запроса)
    if provider in ['helix', 'meowai']:
        if not check_and_increment_limit(user_id, provider):
            await update.message.reply_text('Вы достигли лимита запросов для этого провайдера.', reply_markup=get_main_buttons(user_id, current_model_id))
            return
    # Оповещение о задержке
    await update.message.reply_text("Картинка будет генерироваться чуть дольше из-за необходимости перевода текста на английский.")
    image_url = await generate_image(provider, current_model_id, user_caption)
    if image_url:
        try:
            await update.message.reply_photo(photo=image_url, caption="твоя картинка:", reply_markup=get_main_buttons(user_id, current_model_id))
        except TelegramError as e:
            logger.error(f"Ошибка отправки фото: {e}")
            await update.message.reply_text("Не удалось отправить изображение.")
    else:
        await update.message.reply_text("Не удалось сгенерировать изображение.")
    if os.path.exists(file_path):
        os.remove(file_path)

async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return
    document = update.message.document
    filename = document.file_name.lower()
    if not filename.endswith(('.py', '.json')):
        await update.message.reply_text('Можно отправлять только .py или .json файлы.')
        return
    file_path = os.path.join(SCRIPT_DIR, 'downloads', 'files', document.file_name)
    try:
        await document.get_file().download_to_drive(file_path)
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        os.remove(file_path)
    except Exception as e:
        logger.error(f"Ошибка обработки файла: {e}")
        await update.message.reply_text('Не удалось обработать файл.')
        return
    current_model_id = user_state[user_id].get('current_model_id')
    current_chat_slot = user_state[user_id].get('current_chat_slot')
    if not current_model_id or not current_chat_slot:
        await update.message.reply_text('Модель или чат не выбраны.', reply_markup=get_main_buttons(user_id))
        return
    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai').lower()
    model_type = model_info.get('type', 'text').lower()
    if model_type not in ['text', 'multimodal']:
        await update.message.reply_text('Модель не поддерживает обработку текстовых файлов.', reply_markup=get_main_buttons(user_id, current_model_id))
        return
    # Проверка лимитов для провайдеров (кроме cablyai, где лимит учитывается после запроса)
    if provider in ['helix', 'meowai']:
        if not check_and_increment_limit(user_id, provider):
            await update.message.reply_text('Вы достигли лимита запросов для этого провайдера.', reply_markup=get_main_buttons(user_id, current_model_id))
            return
    context_store.setdefault(user_id, {}).setdefault(current_model_id, {}).setdefault(current_chat_slot, load_chat_context(user_id, current_model_id, current_chat_slot, SYSTEM_INSTRUCTION_TEXT))
    context_store[user_id][current_model_id][current_chat_slot].append({"role": "user", "content": content})
    save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])
    answer = await get_model_response(provider, current_model_id, context_store[user_id][current_model_id][current_chat_slot], model_type, user_id)
    if answer:
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])
        mode = user_mode_store.get(str(user_id), 'message')
        if mode == 'message':
            answer_html = markdown_to_html(answer)
            for msg in split_message(answer_html, MAX_MESSAGE_LENGTH):
                await update.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
        elif mode == 'file':
            file_path_generated = save_md_file(answer, user_id)
            if file_path_generated:
                try:
                    with open(file_path_generated, 'rb') as f:
                        await update.message.reply_document(
                            document=InputFile(f),
                            filename=os.path.basename(file_path_generated),
                            caption="Твой ответ модели в формате markdown.",
                            reply_markup=get_main_buttons(user_id, current_model_id)
                        )
                    os.remove(file_path_generated)
                except Exception as e:
                    logger.error(f"Ошибка отправки файла: {e}")
                    await update.message.reply_text('Не удалось отправить файл.')
            else:
                await update.message.reply_text('Не удалось создать файл с ответом.')
    else:
        if provider == 'cablyai':
            await update.message.reply_text('Не удалось получить ответ от модели или превышен лимит токенов.', reply_markup=get_main_buttons(user_id, current_model_id))
        else:
            await update.message.reply_text('Не удалось получить ответ от модели.', reply_markup=get_main_buttons(user_id, current_model_id))

async def process_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("У вас нет доступа к этому боту.")
        return
    if 'current_provider' not in user_state.get(user_id, {}) or not user_state[user_id].get('current_provider'):
        await update.message.reply_text("Пожалуйста, выберите провайдера из предоставленных кнопок.", reply_markup=get_provider_buttons())
        return
    user_text = update.message.text
    current_model_id = user_state[user_id].get('current_model_id')
    current_chat_slot = user_state[user_id].get('current_chat_slot')
    if not current_model_id or not current_chat_slot:
        await update.message.reply_text('Модель или чат не выбраны.', reply_markup=get_main_buttons(user_id))
        return
    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai').lower()
    model_type = model_info.get('type', 'text').lower()
    # Проверка доступности модели
    if provider == 'cablyai':
        global_tokens = usage_store.get('cablyai_tokens', 0)
        admin_exempt_threshold = config['limits']['cablyai'].get('admin_exempt_threshold', 100000)
        if global_tokens >= config['limits']['cablyai']['daily_token_limit'] - admin_exempt_threshold:
            if user_id not in AUTHORIZED_MODEL_USERS:
                await update.message.reply_text('Доступ к CatAI моделям временно закрыт.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
    elif provider in ['helix', 'meowai']:
        if not check_and_increment_limit(user_id, provider):
            await update.message.reply_text('Вы достигли лимита запросов для этого провайдера.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

    if provider in ['cablyai'] and model_type == 'multimodal' and user_text:
        translated_text = await translate_to_english(user_text)
        if translated_text:
            user_text = translated_text
        else:
            await update.message.reply_text("Не удалось перевести текст.")
            return

    if model_type != 'image':
        context_store.setdefault(user_id, {}).setdefault(current_model_id, {}).setdefault(current_chat_slot, load_chat_context(user_id, current_model_id, current_chat_slot, SYSTEM_INSTRUCTION_TEXT))
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "user", "content": user_text})

    answer = await get_model_response(provider, current_model_id, context_store[user_id][current_model_id][current_chat_slot], model_type, user_id, user_text if model_type == 'image' else None)
    if answer:
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])
        mode = user_mode_store.get(str(user_id), 'message')
        if model_type != 'image':
            if mode == 'message':
                answer_html = markdown_to_html(answer)
                for msg in split_message(answer_html, MAX_MESSAGE_LENGTH):
                    await update.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
            elif mode == 'file':
                file_path_generated = save_md_file(answer, user_id)
                if file_path_generated:
                    try:
                        with open(file_path_generated, 'rb') as f:
                            await update.message.reply_document(
                                document=InputFile(f),
                                filename=os.path.basename(file_path_generated),
                                caption="Твой ответ модели в формате markdown.",
                                reply_markup=get_main_buttons(user_id, current_model_id)
                            )
                        os.remove(file_path_generated)
                    except Exception as e:
                        logger.error(f"Ошибка отправки файла: {e}")
                        await update.message.reply_text('Не удалось отправить файл.')
                else:
                    await update.message.reply_text('Не удалось создать файл с ответом.')
        else:
            if provider == 'cablyai':
                try:
                    await update.message.reply_photo(photo=answer, caption="твоя картинка:", reply_markup=get_main_buttons(user_id, current_model_id))
                except TelegramError as e:
                    logger.error(f"Ошибка отправки изображения: {e}")
                    await update.message.reply_text("Не удалось отправить изображение.")
            else:
                if answer.startswith('http'):
                    try:
                        await update.message.reply_photo(photo=answer, caption="твоя картинка:", reply_markup=get_main_buttons(user_id, current_model_id))
                    except TelegramError as e:
                        logger.error(f"Ошибка отправки изображения: {e}")
                        await update.message.reply_text("Не удалось отправить изображение.")
                else:
                    await update.message.reply_text(answer, reply_markup=get_main_buttons(user_id, current_model_id))
    else:
        if provider == 'cablyai':
            await update.message.reply_text('Не удалось получить ответ от модели или превышен лимит токенов.', reply_markup=get_main_buttons(user_id, current_model_id))
        else:
            await update.message.reply_text('Не удалось получить ответ от модели.', reply_markup=get_main_buttons(user_id, current_model_id))

# Обработчик ошибок
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
    logger.error(msg="Exception while handling an update:", exc_info=context.error)
    if isinstance(update, Update) and update.effective_user:
        try:
            user_id = update.effective_user.id
            current_model_id = user_state.get(user_id, {}).get('current_model_id')
            await update.effective_message.reply_text('Произошла ошибка. Пожалуйста, попробуйте позже.', reply_markup=get_main_buttons(user_id, current_model_id))
        except TelegramError as e:
            logger.error(f"Не удалось отправить сообщение об ошибке пользователю: {e}")

# Фоновая задача для сброса лимитов
async def reset_limits_daily(application):
    while True:
        now = datetime.now(ZoneInfo('Europe/Moscow'))
        reset_time_str = config.get('limit_reset_time', '02:00')
        reset_hour, reset_minute = map(int, reset_time_str.split(':'))
        next_reset = now.replace(hour=reset_hour, minute=reset_minute, second=0, microsecond=0)
        if now >= next_reset:
            next_reset += timedelta(days=1)
        sleep_duration = (next_reset - now).total_seconds()
        logger.info(f"Сброс лимитов через {sleep_duration} секунд.")
        await asyncio.sleep(sleep_duration)
        # Reset global limits
        usage_store['cablyai_tokens'] = 0
        usage_store['helix_requests'] = 0
        # MeowAI не сбрасывается
        # Reset per-user limits for daily limits
        for uid, limits in config.get('user_limits', {}).items():
            if 'cablyai_tokens' in usage_store.get('users', {}).get(uid, {}):
                if limits.get('cablyai', None) != "infinite":
                    usage_store['users'][uid]['cablyai_tokens'] = 0
            if 'helix_requests' in usage_store.get('users', {}).get(uid, {}):
                if limits.get('helix', None) != "infinite":
                    usage_store['users'][uid]['helix_requests'] = 0
            # MeowAI не сбрасывается
        save_json('usage.json', usage_store)
        # Уведомляем пользователей
        for uid in AUTHORIZED_USER_IDS:
            try:
                await application.bot.send_message(chat_id=uid, text="Дневные лимиты сброшены. Вы можете продолжать использовать бота.")
            except Exception as e:
                logger.error(f"Не удалось отправить сообщение пользователю {uid}: {e}")

# Основная функция запуска бота
def main():
    TELEGRAM_TOKEN = config.get('telegram_token')
    if not TELEGRAM_TOKEN:
        logger.error("Токен бота не указан в config.json.")
        exit(1)
    application = ApplicationBuilder().token(TELEGRAM_TOKEN).defaults(Defaults(parse_mode=ParseMode.HTML)).build()

    # Обработчики команд
    application.add_handler(CommandHandler("start", start_command))
    application.add_handler(CommandHandler("setmode", setmode))
    application.add_handler(CommandHandler("mode", mode_command_func))
    application.add_handler(CommandHandler("models", models_command))
    application.add_handler(CommandHandler("reload", reload_bot))
    application.add_handler(CommandHandler("limits", limits_command))
    application.add_handler(CommandHandler("adduser", add_user))
    # Добавьте обработчик команды /instructions
    async def instructions(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """
        Отправляет пользователю инструкции по использованию бота.
        """
        menu_text = (
        "<b>инструкция по использованию бота:</b>\n\n"
        "<b>/start</b> - запустить бота и выбрать модель для взаимодействия.\n"
        "<b>/setmode [message|file]</b> - выбрать режим получения ответов (message - текстовые сообщения, file - .md файлы, они нужны для того чтобы получить полный ответ модели без ограничения длины сообщения телеграма).\n"
        "<b>/mode</b> - узнать текущий выбранный режим.\n"
        "<b>/instructions</b> - получить инструкции по использованию бота.\n"
        "<b>/models</b> - получить описание доступных моделей.\n"
        "<b>/limits</b> - посмотреть ваши текущие лимиты и оставшиеся запросы на сегодня.\n\n"
        "кнопки:\n"
        "<b>сменить модель</b> - выбрать другую модель.\n"
        "<b>продолжить</b> - продолжить ответ модели.\n"
        "‼️<b>токены:</b> токен не равно символ. все модели воспринимают токен как единицу измерения по разному, будь то слово, кусок слова или символ. если про модель указано что она отвечает определённым количеством символов - так и есть.\n"
        "‼️<b>лимиты бота:</b> для CatAI: 2 млн токенов/день на всех юзеров бота;\nдля OpenAI: сейчас недоступен, нету рабочих API ключей;\nдля Helix: 50 запросов/день на всех юзеров бота;\nдля MeowAI: 960 (сейчас) запросов на всех юзеров бота. не восполняются.\nадмин бота не гарантирует, что бот сможет работать бесконечно, всё зависит от провайдеров API ключей. админ постоянно пытается увеличить кол-во провайдеров для большего разнообразия моделей и большей стабильности.\n"
        "‼️<b>контекст:</b> в боте доступны чаты - 2 на каждую модель. ты можешь очистить контекст определённого чата, если надо."
        )
        await update.message.reply_text(menu_text, parse_mode=ParseMode.HTML)
    application.add_handler(CommandHandler("instructions", instructions))

    # Обработчики CallbackQuery
    application.add_handler(CallbackQueryHandler(handle_callback_query, pattern='^(model_select:.+|chat_slot:\d+:.+|change_model|change_provider|change_chat_slot|clear_context|continue)$'))

    # Обработчики сообщений
    application.add_handler(MessageHandler(filters.Regex('^(CatAI|OpenAI|Helix|MeowAI)$') & filters.TEXT, handle_provider_selection))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, process_message))
    application.add_handler(MessageHandler(filters.PHOTO, handle_photo))
    application.add_handler(MessageHandler(filters.Document.ALL, handle_document))

    # Обработчик ошибок
    application.add_error_handler(error_handler)

    # Запуск фоновой задачи для сброса лимитов
    application.job_queue.run_once(lambda context: asyncio.create_task(reset_limits_daily(application)), when=0)

    # Запуск бота
    application.run_polling()

if __name__ == '__main__':
    main()