привет, поможешь улучшить телеграм бота? нужные изменения:
1. увеличение кол-ва чатов до 10. логика переключения остается такой же как сейчас - 1 чат 2 чат кнопка далее, по кнопке далее 3 чат 4 чат кнопка далее кнопка назад, по кнопка далее чаты 5 6 и у них та же логика, с 7 и 8 та же история, а вот где кнопки чатов 9 и 10 там только кнопка назад остаётся.
2. починить .meowlogs команду: не реагирует на указание минут ( типа можно ввести просто .meowlogs и отправится весь лог файл, по идее еще можно ввести .meowlogs 20 и бот отправит логи за последние 20 минут но это не работает )
3. сделать команды /adduser и /reload используемыми через точку.

4. сделать для команды .meowsend возможность отправить сообщение всем пользователям бота: типа .meowsend all мяу! 
5. сделай бота ПОЛНОСТЬЮ АСИНХРОННЫМ. а то сейчас он, если обрабатывает запрос/ждёт ответа модели у провайдера, не обрабатывает запросы от других пользователей/того же пользователя.
с тебя ПОЛНЫЙ ИЗМЕНЁННЫЙ КОД БОТА БЕЗ СОКРАЩЕНИЙ. ВООБЩЕ. а вот сам код:
import os, json, logging, re, html, asyncio, aiohttp, openai, aiofiles
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from io import BytesIO
from PIL import Image
from telegram import (
    Update, InlineKeyboardMarkup, InlineKeyboardButton, InputFile, ReplyKeyboardMarkup
)
from telegram.constants import ParseMode
from telegram.error import TelegramError
from telegram import InputFile as TelegramInputFile
from telegram.ext import (
    Updater, ApplicationBuilder, CommandHandler, MessageHandler, ContextTypes,
    filters, CallbackQueryHandler, Defaults
)
import google.generativeai as genai
from google.api_core import exceptions as google_exceptions

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

# Настройка логирования
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# Создаем Formatter для логов
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')

# Создаем FileHandler для записи логов в файл bot.log (ИСПРАВЛЕНО)
file_handler = logging.FileHandler(os.path.join(SCRIPT_DIR, 'bot.log'), mode='a', encoding='utf-8')
file_handler.setFormatter(formatter)

# Добавляем FileHandler к логгеру
logger.addHandler(file_handler)

def log_action(user_id, action_type, details=None):
    """Записывает действие пользователя в лог."""
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    log_message = f"[{timestamp}] User ID: {user_id} - {action_type}"
    if details:
        log_message += f" - Details: {details}"
    logger.info(log_message)

async def generate_md_log(period=None):
    """
    Генерирует .md файл с логами за указанный период (в минутах).

    Args:
        period: Период времени в минутах, за который нужно получить логи.
               Если None, то возвращаются все логи.

    Returns:
        Путь к созданному .md файлу или None, если произошла ошибка.
    """
    log_file_path = os.path.join(SCRIPT_DIR, "bot.log")

    try:
        async with aiofiles.open(log_file_path, "r", encoding="utf-8") as f:  # Исправлено на aiofiles.open
            logs = await f.readlines() # Добавлен await

        if period:
            time_threshold = datetime.now() - timedelta(minutes=period)
            filtered_logs = []
            for log in logs:
                try:
                    log_time_str = log.split("]")[0][1:]
                    log_time = datetime.strptime(log_time_str, '%Y-%m-%d %H:%M:%S')
                    if log_time >= time_threshold:
                        filtered_logs.append(log)
                except (ValueError, IndexError):
                    # Пропускаем строки с некорректным форматом
                    continue
            logs = filtered_logs

        md_content = "```\n" + "".join(logs) + "```"

        md_file_path = os.path.join(SCRIPT_DIR, f"meowlogs_{datetime.now().strftime('%Y%m%d%H%M%S')}.md")
        async with aiofiles.open(md_file_path, "w", encoding="utf-8") as md_file:  # Исправлено на aiofiles.open
            await md_file.write(md_content) # Добавлен await
        return md_file_path

    except FileNotFoundError:
        return None
    except Exception as e:
        logger.error(f"Ошибка при генерации .md файла лога: {e}")
        return None
    
# Определение директории скрипта и утилиты для работы с JSON
def load_json(file_name, default=None):
    path = os.path.join(SCRIPT_DIR, file_name)
    try:
        with open(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} не найден: {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):
    path = os.path.join(SCRIPT_DIR, file_name)
    try:
        with open(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}")

# Определение промптов в начале файла (глобальная область видимости)
TRANSLATE_IMPROVE_PROMPT = """Улучши и переведи на английский с учётом регистра. 
Попробуй додумать за пользователя, чего он хочет от модели генерации изображений, и верни только улучшенный и переведённый текст:

"""
IMPROVE_PROMPT = """Улучши текст этого сообщения и верни только улучшенный текст:

"""
TRANSLATE_PROMPT = """Переведи следующий текст на английский язык и верни только переведенный текст:

"""

# Загрузка конфигурационных файлов
config = load_json('config.json', default={})
usage_store = load_json('usage.json', default={})
user_mode_store = load_json('user_modes.json', default={})
custom_prompts = load_json('custom_prompts.json', default={})
user_settings_store = load_json('user_settings.json', default={})

# Настройка глобальных переменных
AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))

# Изменено: теперь model_id_to_info содержит только ID и имя модели
model_id_to_info = {info['id']: {'name': name, 'type': info.get('type', 'text')} for name, info in config.get('models', {}).items() if 'id' in info}
model_id_to_name = {info['id']: name for name, info in config.get('models', {}).items() if 'id' in info}

# RESTRICTED_MODELS = {'o1-preview', 'o1-mini', 'openai-o1-preview', 'openai-o1-mini'}
# Убираем ограничение на модели, так как теперь провайдеры не привязаны жестко
RESTRICTED_MODELS = set()

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 = {} # Это больше не нужно

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

# Отображение названий провайдеров
provider_display_names = {
    'cablyai': 'CatAI',
    'openai': 'OpenAI',
    'helix': 'Helix',
    'meowai': 'MeowAI',
    'rimunace': 'Rimunace',
    'nobrandai': 'NoBrandAI',
    'fresed': 'Fresed',
    'anyai': 'AnyAI',
    'shardai': 'ShardAI',
    'clashai': 'ClashAI',
    'skailar': 'Skailar',
    'voidai': 'VoidAI',
    'kraken': 'Kraken',
    'google': 'Google'
}

# Изменено: теперь model_providers хранится в config.json
model_providers = config.get('model_providers', {})

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

def save_chat_context(user_id, model_id, chat_slot, context):
    # Изменено: убрано provider из пути
    path = os.path.join(SCRIPT_DIR, 'chats', f"{user_id}_{model_id}_chat{chat_slot}.json")
    try:
        with open(path, '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, model_id):
    """
    Проверяет, не превышен ли лимит для пользователя и модели, и увеличивает счетчик использования.

    Args:
        user_id: ID пользователя.
        model_id: ID модели.

    Returns:
        True, если лимит не превышен, False в противном случае.
    """
    model_limits = config['model_limits'].get(model_id)
    if not model_limits:
        return True  # Нет лимитов для модели

    user_limits = config.get('user_limits', {}).get(str(user_id), {})
    user_model_limit = user_limits.get(model_id, {}).get("per_user_daily_requests", "infinite")

    # Проверка общего лимита
    total_requests_key = f'{model_id}_total_requests'
    total_requests = usage_store.get(total_requests_key, 0)
    if total_requests >= model_limits['total_daily_requests'] and user_id not in AUTHORIZED_MODEL_USERS:
        return False

    # Проверка лимита пользователя
    user_requests_key = f'{model_id}_requests'
    user_requests = usage_store.get('users', {}).get(str(user_id), {}).get(user_requests_key, 0)
    if user_model_limit != "infinite" and user_requests >= user_model_limit:
        return False

    # Увеличение счетчиков
    usage_store.setdefault('users', {}).setdefault(str(user_id), {}).setdefault(user_requests_key, 0)
    usage_store[total_requests_key] = usage_store.get(total_requests_key, 0) + 1
    if user_model_limit != "infinite":
        usage_store['users'][str(user_id)][user_requests_key] += 1

    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=4096):
    return [message[i:i+max_length] for i in range(0, len(message), max_length)]

def get_buttons(layout):
    return ReplyKeyboardMarkup(layout, resize_keyboard=True, one_time_keyboard=False)

# Изменено: теперь создает кнопки для семейств моделей
def get_model_family_buttons():
    families = [
        "⚡️ модели GPT",  # Добавлен эмодзи, изменен регистр
        "🧠 модели Gemini", # Добавлен эмодзи, изменен регистр
        "☁️ модели Claude", # Добавлен эмодзи, изменен регистр
        "💥 модели o1",    # Добавлен эмодзи, изменен регистр
        "🦙 модели LLaMA", # Добавлен эмодзи, изменен регистр
        "🔀 другие модели"  # Добавлен эмодзи, изменен регистр
    ]
    buttons = []
    for i in range(0, len(families), 2):
        row = []
        row.append(InlineKeyboardButton(families[i], callback_data=families[i]))
        if i + 1 < len(families):
            row.append(InlineKeyboardButton(families[i+1], callback_data=families[i+1]))
        buttons.append(row)
    buttons.append([InlineKeyboardButton("🖼️ модели для картинок", callback_data="🖼️ модели для картинок")])
    return ReplyKeyboardMarkup(buttons, resize_keyboard=True, one_time_keyboard=False)

def get_inline_buttons(buttons):
    return InlineKeyboardMarkup(buttons)

# Изменено: убрана логика выбора провайдера, выбираем только модель
def get_model_buttons(family):
    buttons = []
    for model_id, info in model_id_to_info.items():
        model_name = info['name']
        model_type = info.get('type', 'text')  # Получаем тип модели

        if family == "⚡️ модели GPT" and "gpt" in model_name.lower():
            buttons.append([InlineKeyboardButton(model_name, callback_data=f"model_select:{model_id}")])
        elif family == "🧠 модели Gemini" and "gemini" in model_name.lower():
            buttons.append([InlineKeyboardButton(model_name, callback_data=f"model_select:{model_id}")])
        elif family == "☁️ модели Claude" and ("claude" in model_name.lower() or "сlaude" in model_name.lower()):
            buttons.append([InlineKeyboardButton(model_name, callback_data=f"model_select:{model_id}")])
        elif family == "💥 модели o1" and "o1" in model_name.lower():
            buttons.append([InlineKeyboardButton(model_name, callback_data=f"model_select:{model_id}")])
        elif family == "🦙 модели LLaMA" and "llama" in model_name.lower():
            buttons.append([InlineKeyboardButton(model_name, callback_data=f"model_select:{model_id}")])
        elif family == "🖼️ модели для картинок" and model_type == 'image':
            buttons.append([InlineKeyboardButton(model_name, callback_data=f"model_select:{model_id}")])
        elif family == "🔀 другие модели":
            # Проверяем, что модель не относится к другим семействам
            if not any(keyword in model_name.lower() for keyword in ["gpt", "gemini", "claude", "сlaude", "o1", "llama"]) and model_type != 'image':
                buttons.append([InlineKeyboardButton(model_name, callback_data=f"model_select:{model_id}")])

    buttons.append([InlineKeyboardButton("Назад", callback_data='back_to_families')])
    return InlineKeyboardMarkup(buttons)

def get_chat_buttons(model_id, page=1):
    """
    Возвращает кнопки для выбора чата с пагинацией.
    На странице 1: Чат 1, Чат 2, Далее
    На странице 2: Чат 3, Чат 4, Назад
    Все кнопки располагаются в одну строку.
    """
    if page == 1:
        buttons = [
            InlineKeyboardButton("Чат 1", callback_data=f"chat_slot:1:{model_id}"),
            InlineKeyboardButton("Чат 2", callback_data=f"chat_slot:2:{model_id}"),
            InlineKeyboardButton("далее", callback_data='next_chats')
        ]
    elif page == 2:
        buttons = [
            InlineKeyboardButton("Чат 3", callback_data=f"chat_slot:3:{model_id}"),
            InlineKeyboardButton("Чат 4", callback_data=f"chat_slot:4:{model_id}"),
            InlineKeyboardButton("назад", callback_data='back_chats')
        ]
    else:
        buttons = []
    return InlineKeyboardMarkup([buttons])

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

def is_image_supported(model_id):
    # Изменено: теперь проверяем по типу модели
    model_info = model_id_to_info.get(model_id)
    if model_info:
        return model_info.get('type') == 'image'
    return False

def get_api_model_id(model_id, provider):
    if provider == 'cablyai' and model_id == 'claude-3.5-sonnet-20241022':
        return model_id.replace("3.5", "3-5")
    return model_id

# Асинхронные функции API запросов
async def send_api_request(provider, model_id, messages=None, prompt=None, model_type='text'):
    """
    Отправляет запрос к API выбранного провайдера и возвращает ответ.
    """
    provider_info = config['providers'].get(provider)
    if not provider_info:
        logger.error(f"Настройки провайдера '{provider}' отсутствуют в config.json.")
        return None

    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 and provider != 'google':
        logger.error(f"API URL для провайдера '{provider}' не найден в config.json.")
        return None

    headers = {'Content-Type': 'application/json'}
    if provider_info.get('api_key') and provider != 'google':
        headers['Authorization'] = f"Bearer {provider_info['api_key']}"

    data = {}

    if provider == 'google':
        # Используем библиотеку google-generativeai
        for api_key in provider_info['api_keys']:
            try:
                genai.configure(api_key=api_key)
                model = genai.GenerativeModel(model_id)
                logger.info(f"Отправка запроса к Google API. Модель: {model_id}, Ключ: {api_key}, Тип: {model_type}")

                if messages:
                    if model_type == 'multimodal':
                        # Преобразуем сообщения для мультимодальной модели
                        contents = []
                        for msg in messages:
                            if msg["role"] == "user":
                                # Проверяем, есть ли в сообщении текст или изображение
                                if isinstance(msg["content"], str):
                                    contents.append({"role": "user", "parts": [{"text": msg["content"]}]})
                                elif isinstance(msg["content"], list):
                                    parts = []
                                    for item in msg["content"]:
                                        if isinstance(item, dict) and item.get("type") == "text":
                                            parts.append({"text": item["text"]})
                                        elif isinstance(item, dict) and item.get("type") == "image_url":
                                            # Скачиваем изображение
                                            async with aiohttp.ClientSession() as session:
                                                async with session.get(item["image_url"]["url"]) as resp:
                                                    if resp.status == 200:
                                                        image_bytes = await resp.read()
                                                        image = Image.open(BytesIO(image_bytes))
                                                        # Добавляем изображение в parts
                                                        parts.append(image)
                                                    else:
                                                        logger.error(f"Ошибка при скачивании изображения: {resp.status}")
                                    contents.append({"role": "user", "parts": parts})
                            elif msg["role"] == "system":
                                contents.append({"role": "user", "parts": [{"text": msg["content"]}]})
                            elif msg["role"] == "assistant":
                                contents.append({"role": "model", "parts": [{"text": msg["content"]}]})
                        logger.info(f"Преобразованные мультимодальные сообщения: {contents}")
                        response = await model.generate_content_async(contents)

                    else:
                        # Для текстовых моделей используем оригинальные сообщения
                        response = await model.generate_content_async(messages)

                    logger.info(f"Получен ответ от Google API: {response.text}")
                    return response.text # УСПЕШНЫЙ ЗАПРОС - сразу возвращаем ответ

                elif prompt:
                    response = await model.generate_content_async(prompt)
                    logger.info(f"Получен ответ от Google API: {response.text}")
                    return response.text # УСПЕШНЫЙ ЗАПРОС - сразу возвращаем ответ

            except google_exceptions.GoogleAPIError as e:
                logger.error(f"Ошибка Google API с ключом {api_key}: {e}")
                # Ошибки не возвращаем, продолжаем цикл по ключам
            except Exception as e:
                logger.error(f"Непредвиденная ошибка Google API: {e}")
                # Ошибки не возвращаем, продолжаем цикл по ключам

        logger.error("Не удалось получить ответ от Google API после перебора всех ключей.")
        return None  # Возвращаем None, если не удалось получить ответ от Google API

    elif provider == 'anyai':
        api_url = provider_info.get('api_endpoint_text') or provider_info.get('api_endpoint')
        headers = {'Content-Type': 'application/json'}
        if provider_info.get('api_key'):
            headers['Authorization'] = f"Bearer {provider_info['api_key']}"

        # Убираем префикс у anyai
        model_id = model_id.split("anyai-", 1)[-1]

        data = {"model": model_id, "messages": messages}

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

        try:
            timeout = aiohttp.ClientTimeout(total=300)
            async with aiohttp.ClientSession(timeout=timeout) 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"Ответ от AnyAI API: {response_json}")
                        return response_json
                    else:
                        error_text = await response.text()
                        logger.error(f"Ошибка запроса к AnyAI API: {response.status} - {error_text}")
                        return None
        except asyncio.TimeoutError:
            logger.error(f"Запрос к AnyAI API превысил таймаут.")
            return None
        except Exception as e:
            logger.error(f"Ошибка при запросе к AnyAI API: {e}")
            return None
    else:
        if model_type in ['text', 'multimodal']:
            data["messages"] = messages
        elif model_type == 'image':
            data.update({"prompt": prompt, "n": 1, "size": "1024x1024"}) # добавим model
        else:
            logger.error(f"Неизвестный тип модели: {model_type}")
            return None

        # Ищем переопределение model_id для провайдера
        model_id_override = config.get('model_id_overrides', {}).get(provider, {}).get(model_id)
        api_model_id = model_id_override or model_id  # Если переопределение есть, используем его, иначе оригинальный model_id

        # Убираем префикс, если он есть
        for prefix in [
            'cablyai-', 'helix-', 'meowai-', 'rimunace-', 'nobrand-', 'fresed-',
            'anyai-', 'shard-', 'clashai-', 'skailar-', 'voidai-', 'kraken-', 'google-', 'openai-'
        ]:
            if api_model_id.startswith(prefix):
                api_model_id = api_model_id.replace(prefix, "", 1)
                break
        
        # Добавляем model в data для остальных провайдеров
        data["model"] = api_model_id

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

        try:
            timeout = aiohttp.ClientTimeout(total=300)
            async with aiohttp.ClientSession(timeout=timeout) 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 asyncio.TimeoutError:
            logger.error(f"Запрос к {provider} API превысил таймаут.")
            return None
        except Exception as e:
            logger.error(f"Ошибка при запросе к {provider} API: {e}")
            return None
      
async def translate_improve_text(action, text, user_id, context_obj):
    # Порядок важен: сначала пробуем rimunace, потом helix
    model_configs = [
        {
            "model_id": "gpt-4-turbo",
            "prompt": TRANSLATE_IMPROVE_PROMPT if action == 'translate_improve' else IMPROVE_PROMPT if action == 'improve' else TRANSLATE_PROMPT,
            "system_content": "Ты помогаешь изменять текст сообщений к специальным моделям генерации изображений."
        },
        {
            "model_id": "chatgpt-4o-latest",
            "prompt": TRANSLATE_IMPROVE_PROMPT if action == 'translate_improve' else IMPROVE_PROMPT if action == 'improve' else TRANSLATE_PROMPT,
            "system_content": "ты переводишь русский текст на английский." if action == 'translate' else "ты помогаешь улучшать текст сообщения к специальным моделям генерации изображений." if action == 'improve' else "ты помогаешь улучшать и переводить на английский текст сообщения к специальным моделям генерации изображений."
        }
    ]

    for model_config in model_configs:
        model_id = model_config["model_id"]
        # Автоматически выбираем провайдера
        providers = model_providers.get(model_id)
        if not providers:
            logger.error(f"Для модели {model_id} не найдено провайдеров.")
            continue
          
        provider = providers[0] # Выбираем первого доступного провайдера

        messages = [
            {"role": "system", "content": model_config["system_content"]},
            {"role": "user", "content": f"{model_config['prompt']}{text}"}
        ]

        response = await send_api_request(provider, model_id, messages=messages, model_type='multimodal')

        if response and "choices" in response and response["choices"]:
            content = response["choices"][0].get('message', {}).get('content', '').strip()
            if content:  # Проверяем, что строка не пустая
                return content

    logger.error(f"Ошибка при {action} текста.")
    return None

async def generate_image(provider, model_id, prompt, user_id, context_obj):
    # Изменено: добавлена поддержка Google (возвращает текст)
    if provider == 'google':
        response = await send_api_request(provider, model_id, prompt=prompt, model_type='image')
        if response:
            # Google возвращает текст, а не URL, поэтому отправляем его как текст
            await context_obj.bot.send_message(chat_id=user_id, text=response)
        else:
            await context_obj.bot.send_message(chat_id=user_id, text="Не удалось сгенерировать изображение.")
        return None

    if provider == 'cablyai' and usage_store.get('cablyai_images_generated', 0) >= config['limits']['cablyai']['image_daily_limit']:
        await context_obj.bot.send_message(chat_id=user_id, text="достигнут дневной лимит генерации изображений.")
        return None
    response = await send_api_request(provider, model_id, prompt=prompt, model_type='image')
    if response and 'data' in response and response['data']:
        image_url = response['data'][0].get('url')
        if provider == 'cablyai':
            usage_store['cablyai_images_generated'] += 1
            save_json('usage.json', usage_store)
        return image_url
    logger.error(f"Ошибка генерации изображения через {provider} API.")
    return None

async def get_model_response(provider, model_id, messages, model_type, user_id, prompt=None):
    # Ищем переопределение model_id для провайдера
    model_id_override = config.get('model_id_overrides', {}).get(provider, {}).get(model_id)
    api_model_id = model_id_override or model_id

    if provider == 'google':
        response = await send_api_request(provider, api_model_id, messages=messages, prompt=prompt, model_type=model_type)
        return response  # Google возвращает текст, сразу возвращаем

    # ИЗМЕНЕНИЯ ТУТ:
    if provider in ['helix', 'meowai', 'rimunace', 'nobrandai', 'fresed', 'anyai', 'shardai', 'clashai', 'skailar', 'voidai', 'kraken', 'google']:
        response = await send_api_request(provider, api_model_id, messages=messages, prompt=prompt, model_type=model_type)
        if response:
            if model_type == 'image':
                # Возвращаем URL изображения для ClashAI и других провайдеров, у которых 'data' - список словарей
                return response.get('data', [{}])[0].get('url', None)
            elif "choices" in response and response["choices"]:
                return response["choices"][0].get('message', {}).get('content', None)
    # КОНЕЦ ИЗМЕНЕНИЙ

    elif provider == 'cablyai':
        response = await send_api_request(provider, api_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"]:
            if 'usage' in response and 'total_tokens' in response['usage']:
                total_tokens = response['usage']['total_tokens']
                usage_store['cablyai_tokens'] += total_tokens
                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 = model_id
        try:
            if model_type == 'image' and prompt:
                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

# Обработчики команд и сообщений
async def add_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
    admin_id = 5005816138
    user_id = update.effective_user.id
    log_action(user_id, "Команда", f"/adduser {context.args}")
    if 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 числовой id юзера")
        return
    new_user = int(context.args[0])
    if new_user in AUTHORIZED_USER_IDS:
        await update.message.reply_text("этот пользователь уже есть в config.json.")
        return

    # Добавляем пользователя в AUTHORIZED_USER_IDS
    AUTHORIZED_USER_IDS.add(new_user)

    # Добавляем пользователя в config.json (с поправкой под список)
    config.setdefault('authorized_user_ids', []).append(new_user)

    # Добавляем пользователя в user_limits
    config.setdefault('user_limits', {})[str(new_user)] = {
        "cablyai": 200000, "helix": 15, "meowai": 50, "rimunace": 15, "nobrandai": 10, "fresed": 15, "anyai": 20, "shardai": 48, "clashai": 10, "skailar": 10, "voidai": 10, "kraken": 10, "google": 50
    }

    # Добавляем пользователя в usage_store
    usage_store.setdefault('users', {}).setdefault(str(new_user), {}).update({
        "cablyai_tokens": 0,
        "helix_requests": 0,
        "meowai_requests": 0,
        "rimunace_requests": 0,
        "nobrandai_credits": 0,
        "fresed_requests": 0,
        "anyai_requests": 0,
        "shardai_credits": 0,
        "clashai_requests": 0,
        "skailar_requests": 0,
        "voidai_requests": 0,
        "kraken_requests": 0,
        "google_requests": 0
    })

    save_json('config.json', config)
    save_json('usage.json', usage_store) # Добавлено сохранение usage_store

    await update.message.reply_text(f"пользователь {new_user} добавлен!")
    try:
        await context.bot.send_message(chat_id=new_user, text="у тебя теперь есть доступ к боту.")
    except Exception as e:
        logger.error(f"не получилось отправить сообщение новому пользователю {new_user}: {e}")

async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Команда", "/settings")
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return
    settings = user_settings_store.get(str(user_id), {"autotranslation": True, "autoimprovement": True})
    keyboard = [
        [InlineKeyboardButton(
            f"Автоперевод запросов генерации картинок {'✅' if settings['autotranslation'] else '❌'}",
            callback_data="toggle_autotranslation"
        )],
        [InlineKeyboardButton(
            f"Автоулучшение запроса генерации картинок {'✅' if settings['autoimprovement'] else '❌'}",
            callback_data="toggle_autoimprovement"
        )]
    ]
    await update.message.reply_text("Твои настройки:", reply_markup=InlineKeyboardMarkup(keyboard))

async def handle_callback_query(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    user_id = query.from_user.id
    log_action(user_id, "Callback запрос", query.data)

    if user_id not in AUTHORIZED_USER_IDS:
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    data = query.data
    state = user_state.setdefault(user_id, {})

    if data.startswith("family_select:"):
        family = data.split(":")[1]
        await query.message.reply_text(
            f'выбрано семейство: {family}\n\nвыбери модель:',
            parse_mode=ParseMode.HTML,
            reply_markup=get_model_buttons(family)
        )

    elif data in {"toggle_autotranslation", "toggle_autoimprovement"}:
        setting = "autotranslation" if data == "toggle_autotranslation" else "autoimprovement"
        settings = user_settings_store.get(str(user_id), {"autotranslation": True, "autoimprovement": True})
        settings[setting] = not settings.get(setting, True)
        user_settings_store[str(user_id)] = settings
        save_json('user_settings.json', user_settings_store)
        emoji = '✅' if settings[setting] else '❌'
        text = f"{'Автоперевод запросов генерации картинок' if setting == 'autotranslation' else 'Автоулучшение запроса генерации картинок'} {'включен' if settings[setting] else 'выключен'}."
        keyboard = [
            [InlineKeyboardButton(
                f"Автоперевод запросов генерации картинок {emoji}",
                callback_data="toggle_autotranslation"
            )],
            [InlineKeyboardButton(
                f"Автоулучшение запроса генерации картинок {'✅' if settings['autoimprovement'] else '❌'}",
                callback_data="toggle_autoimprovement"
            )]
        ]
        await query.edit_message_text("твои настройки:", reply_markup=InlineKeyboardMarkup(keyboard))
        await context.bot.send_message(chat_id=user_id, text=text)

    elif data.startswith("model_select:"):
        model_id_from_callback = data.split(":")[1]  # ID из callback_data
        logger.info(f"Выбран model_id из callback_data: {model_id_from_callback}")

        # Ищем model_id в model_id_to_info по ID из callback_data
        model_id = None
        for m_id, m_info in model_id_to_info.items():
            if m_id == model_id_from_callback:
                model_id = m_id
                model_name = m_info['name']
                logger.info(f"Найден model_id: {model_id} для имени модели: {model_name}")
                break

        if not model_id:
            logger.error(f"Неизвестный model_id: {model_id_from_callback}")
            await query.message.reply_text('Неизвестная модель.')
            return

        # Автоматически выбираем провайдера
        providers = model_providers.get(model_id)
        if not providers:
            logger.error(f"Не найдено провайдеров для model_id: {model_id}")
            await query.message.reply_text(f'Для модели {model_name} не найдено провайдеров.')
            return

        provider = providers[0] # Выбираем первого провайдера
        logger.info(f"Выбран провайдер: {provider} для model_id: {model_id}")

        model_id_override = config.get('model_id_overrides', {}).get(provider, {}).get(model_id)
        api_model_id = model_id_override or model_id

        user_state[user_id].update({'current_model_id': model_id, 'current_provider': provider, 'current_page': 1})

        await context.bot.send_message(
            chat_id=user_id,
            text=f'выбрана модель: <b>{html.escape(model_name)}</b>\n\nвыбери слот чата:', # Исправлено: используем model_name
            parse_mode=ParseMode.HTML,
            reply_markup=get_chat_buttons(model_id, page=1)
        )

    elif data.startswith("chat_slot:"):
        # model_id берем as is
        _, slot, selected_model_id = data.split(":")
        slot = int(slot)
        user_state[user_id].update({'current_chat_slot': slot, 'current_page': 1})
        system_instruction = custom_prompts.get(str(user_id), SYSTEM_INSTRUCTION_TEXT) if selected_model_id not in RESTRICTED_MODELS else SYSTEM_INSTRUCTION_TEXT
        context_store.setdefault(user_id, {}).setdefault(selected_model_id, {}).setdefault(slot, load_chat_context(user_id, selected_model_id, slot, system_instruction))
        # model_id_to_name.get(selected_model_id, "неизвестная модель") - model_id as is
        chat_message = f'выбран чат {slot} для модели {html.escape(model_id_to_name.get(selected_model_id, "неизвестная модель"))}'
        short_description = MODEL_SHORT_DESCRIPTIONS.get(selected_model_id)
        if short_description:
            chat_message += f'\n\n{short_description}'

        await context.bot.send_message(
            chat_id=user_id,
            text=chat_message,
            parse_mode=ParseMode.HTML,
            reply_markup=get_inline_buttons([
                [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                 InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                 InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
            ])
        )

    # Изменено: убран выбор провайдера, вместо него выбор семейства моделей
    elif data == 'change_model':
        await context.bot.send_message(
            chat_id=user_id,
            text='выбери семейство моделей:',
            parse_mode=ParseMode.HTML,
            reply_markup=get_model_family_buttons()
        )

    # Изменено: кнопка возврата к выбору семейств моделей
    elif data == 'back_to_families':
        user_state[user_id].update({'current_model_id': None, 'current_provider': None, 'current_chat_slot': None, 'current_page': 1})
        await context.bot.send_message(
            chat_id=user_id,
            text='выбери семейство моделей:',
            parse_mode=ParseMode.HTML,
            reply_markup=get_model_family_buttons()
        )

    elif data == 'clear_context':
        state = user_state.get(user_id, {})
        # model_id берем as is
        model_id = state.get('current_model_id')
        slot = state.get('current_chat_slot')
        if not model_id or not slot:
            await query.message.reply_text('модель или чат не выбраны.')
            return
        system_instruction = custom_prompts.get(str(user_id), SYSTEM_INSTRUCTION_TEXT) if model_id not in RESTRICTED_MODELS else SYSTEM_INSTRUCTION_TEXT
        context_store[user_id][model_id][slot] = [{"role": "system", "content": system_instruction}]
        save_chat_context(user_id, model_id, slot, context_store[user_id][model_id][slot])
        # model_id_to_name.get(model_id, "неизвестная модель") - model_id as is
        await context.bot.send_message(
            chat_id=user_id,
            text=f'контекст <b>чата {slot}</b> для модели <b>{html.escape(model_id_to_name.get(model_id, "неизвестная модель"))}</b> очищен.',
            parse_mode=ParseMode.HTML,
            reply_markup=get_inline_buttons([
                [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                 InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                 InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
            ])
        )

    elif data == 'continue':
        await query.answer()
        state = user_state.get(user_id, {})
        model_id = state.get('current_model_id')
        slot = state.get('current_chat_slot')
        if not model_id or not slot:
            await query.answer(text='модель или чат не выбраны.', show_alert=True)
            return

        providers = model_providers.get(model_id, [])
        model_type = model_id_to_info.get(model_id, {}).get('type', 'text').lower()
        context_messages = context_store[user_id][model_id][slot]
        context_messages.append({"role": "user", "content": "продолжи"})
        
        answer = None
        for provider in providers:
            model_id_override = config.get('model_id_overrides', {}).get(provider, {}).get(model_id)
            api_model_id = model_id_override or model_id
            
            answer = await get_model_response(provider, api_model_id, context_messages, model_type, user_id)
            if answer:
                check_and_increment_limit(user_id, model_id)
                break

        if answer:
            context_messages.append({"role": "assistant", "content": answer})
            save_chat_context(user_id, model_id, slot, context_messages)
            if model_type != 'image':
                for msg in split_message(markdown_to_html(answer), MAX_MESSAGE_LENGTH):
                    await context.bot.send_message(chat_id=user_id, text=msg, reply_markup=get_inline_buttons([
                        [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                            InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                            InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                        [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                    ]))
            else:
                try:
                    await context.bot.send_photo(chat_id=user_id, photo=answer, caption="Твоя картинка:", reply_markup=get_inline_buttons([
                        [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                            InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                            InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                        [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                    ]))
                except TelegramError as e:
                    logger.error(f"Ошибка отправки изображения: {e}")
                    await context.bot.send_message(chat_id=user_id, text="Не удалось отправить изображение.")
        else:
            await context.bot.send_message(
                chat_id=user_id,
                text='<b>ни один провайдер не смог обработать запрос для выбранной модели.</b>\nтакое ОЧЕНЬ маловероятно. попробуй: очистить контекст; прописать /start; сменить модель.',
                parse_mode=ParseMode.HTML,
                reply_markup=get_inline_buttons([
                    [InlineKeyboardButton("сменить модель", callback_data='change_model')]
                ])
            )

    elif data == 'change_chat_slot':
        # model_id берем as is
        model_id = state.get('current_model_id')
        if not model_id:
            await query.message.reply_text('модель не выбрана.')
            return
        # model_id_to_name.get(model_id, "неизвестная модель") - model_id as is
        await context.bot.send_message(
            chat_id=user_id,
            text=f'выбери слот чата для модели <b>{html.escape(model_id_to_name.get(model_id, "неизвестная модель"))}</b>:',
            parse_mode=ParseMode.HTML,
            reply_markup=get_chat_buttons(model_id, page=1)
        )

    elif data == 'next_chats':
        model_id = state.get('current_model_id')
        page = state.get('current_page', 1)
        if page < 2:
            user_state[user_id]['current_page'] = page + 1
            await query.message.edit_reply_markup(reply_markup=get_chat_buttons(model_id, page = page+1))

    elif data == 'back_chats':
        model_id = state.get('current_model_id')
        page = state.get('current_page', 1)
        if page > 1:
            user_state[user_id]['current_page'] = page - 1
            await query.message.edit_reply_markup(reply_markup=get_chat_buttons(model_id, page = page-1))

    else:
        await query.message.reply_text('неизвестная команда.')

# Изменено: обработчик выбора семейства моделей
async def handle_model_family_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_text = update.message.text
    user_id = update.effective_user.id
    log_action(user_id, "Выбор семейства моделей", user_text)

    # Добавляем проверку на family_select:
    family = user_text
    await update.message.reply_text(
        f'выбрано семейство: {family}\n\nвыбери модель:',
        parse_mode=ParseMode.HTML,
        reply_markup=get_model_buttons(family)
    )

async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Запрос с фото", update.message.caption)
    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
    caption = update.message.caption or ""
    state = user_state.get(user_id, {})
    model_id = state.get('current_model_id')
    slot = state.get('current_chat_slot')
    if not model_id or not slot:
        await update.message.reply_text('модель или чат не выбраны.', reply_markup=get_inline_buttons([
            [InlineKeyboardButton("сменить модель", callback_data='change_model')]
        ]))
        return

    model = model_id_to_info.get(model_id, {})
    model_type = model.get('type', 'text').lower()
    is_multimodal = model_type == 'multimodal'

    providers = model_providers.get(model_id, [])
    answer = None
    for provider in providers:
        model_id_override = config.get('model_id_overrides', {}).get(provider, {}).get(model_id)
        api_model_id = model_id_override or model_id

        context_store.setdefault(user_id, {}).setdefault(model_id, {}).setdefault(slot, load_chat_context(user_id, model_id, slot, SYSTEM_INSTRUCTION_TEXT))
        if is_multimodal:
            context_store[user_id][model_id][slot].append(
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "text",
                            "text": caption if caption else "Вот, держи"
                        },
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": file.file_path
                            }
                        }
                    ]
                }
            )
            save_chat_context(user_id, model_id, slot, context_store[user_id][model_id][slot])

            answer = await get_model_response(provider, api_model_id, context_store[user_id][model_id][slot], 'multimodal', user_id)
            if answer:
                check_and_increment_limit(user_id, model_id)
                break
        else:
            await update.message.reply_text('Выбранная модель не предназначена для обработки изображений.', reply_markup=get_inline_buttons([
                [InlineKeyboardButton("сменить модель", callback_data='change_model')]
            ]))
            return

    if os.path.exists(file_path):
        os.remove(file_path)

    if answer:
        try:
            await update.message.reply_text(answer, reply_markup=get_inline_buttons([
                [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                    InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                    InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
            ]))
        except TelegramError as e:
            logger.error(f"Ошибка отправки ответа: {e}")
            await update.message.reply_text("Не удалось отправить ответ.")
    else:
        await update.message.reply_text(
            '<b>ни один провайдер не смог обработать запрос для выбранной модели.</b>\nтакое ОЧЕНЬ маловероятно. попробуй: очистить контекст; прописать /start; сменить модель.',
            parse_mode=ParseMode.HTML,
            reply_markup=get_inline_buttons([
                [InlineKeyboardButton("сменить модель", callback_data='change_model')]
            ])
        )
        
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    file_name = update.message.document.file_name
    mime_type = update.message.document.mime_type
    log_action(user_id, "Запрос с документом", f"File: {file_name}, Mime-type: {mime_type}")
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return
    document = update.message.document

    if not document.mime_type.startswith('text/'):
        await update.message.reply_text('можно отправлять только текстовые файлы.')
        return

    try:
        file = await context.bot.get_file(document.file_id)
        async with aiohttp.ClientSession() as session:
            async with session.get(file.file_path) as response:
                if response.status == 200:
                    content = await response.text(encoding='utf-8')
                else:
                    raise Exception(f"Ошибка загрузки файла: {response.status}")

    except Exception as e:
        logger.error(f"Ошибка обработки файла: {e}")
        await update.message.reply_text('не удалось обработать файл.')
        return

    state = user_state.get(user_id, {})
    model_id = state.get('current_model_id')
    slot = state.get('current_chat_slot')
    if not model_id or not slot:
        await update.message.reply_text('модель или чат не выбраны.', reply_markup=get_inline_buttons([
            [InlineKeyboardButton("сменить модель", callback_data='change_model')]
        ]))
        return

    model = model_id_to_info.get(model_id, {})
    model_type = model.get('type', 'text').lower()

    if model_type not in {'multimodal', 'text'}:
        await update.message.reply_text('модель не поддерживает обработку текстовых файлов.', reply_markup=get_inline_buttons([
            [InlineKeyboardButton("сменить модель", callback_data='change_model')]
        ]))
        return

    providers = model_providers.get(model_id, [])
    answer = None
    for provider in providers:
        model_id_override = config.get('model_id_overrides', {}).get(provider, {}).get(model_id)
        api_model_id = model_id_override or model_id

        context_store.setdefault(user_id, {}).setdefault(model_id, {}).setdefault(slot, load_chat_context(user_id, model_id, slot, SYSTEM_INSTRUCTION_TEXT))
        context_store[user_id][model_id][slot].append({"role": "user", "content": content})
        save_chat_context(user_id, model_id, slot, context_store[user_id][model_id][slot])
        answer = await get_model_response(provider, api_model_id, context_store[user_id][model_id][slot], model_type, user_id)

        if answer:
            check_and_increment_limit(user_id, model_id)
            break

    if answer:
        context_store[user_id][model_id][slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, model_id, slot, context_store[user_id][model_id][slot])
        mode = user_mode_store.get(str(user_id), 'message')

        if model_type != 'image':
            if mode == 'message':
                for msg in split_message(markdown_to_html(answer), MAX_MESSAGE_LENGTH):
                    await update.message.reply_text(msg, reply_markup=get_inline_buttons([
                        [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                            InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                            InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                        [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                    ]))
            elif mode == 'file':
                path = save_md(answer, user_id)
                if path:
                    try:
                        with open(path, 'rb') as f:
                            await update.message.reply_document(
                                document=InputFile(f),
                                filename=os.path.basename(path),
                                caption="твой ответ модели в формате markdown.",
                                reply_markup=get_inline_buttons([
                                    [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                                        InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                                        InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                                    [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                                ])
                            )
                        os.remove(path)
                    except Exception as e:
                        logger.error(f"Ошибка отправки файла: {e}")
                        await update.message.reply_text('не получилось отправить файл.')
                else:
                    await update.message.reply_text('не получилось создать файл с ответом.')
        else:
            try:
                await update.message.reply_photo(photo=answer, caption="Твоя картинка:", reply_markup=get_inline_buttons([
                    [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                        InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                        InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                    [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                ]))
            except TelegramError as e:
                logger.error(f"Ошибка отправки изображения: {e}")
                await update.message.reply_text("Не удалось отправить изображение.")
    else:
        await update.message.reply_text(
            '<b>ни один провайдер не смог обработать запрос для выбранной модели.</b>\nтакое ОЧЕНЬ маловероятно. попробуй: очистить контекст; прописать /start; сменить модель.',
            parse_mode=ParseMode.HTML,
            reply_markup=get_inline_buttons([
                [InlineKeyboardButton("сменить модель", callback_data='change_model')]
            ])
        )

async def process_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Запрос", update.message.text)
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("у тебя нет доступа к этому боту.")
        return

    if not user_state.get(user_id, {}).get('current_model_id'):
        await update.message.reply_text("выбери семейство моделей.", reply_markup=get_model_family_buttons())
        return

    user_text = update.message.text
    state = user_state.get(user_id, {})
    model_id = state.get('current_model_id')
    slot = state.get('current_chat_slot')

    if not model_id or not slot:
        await update.message.reply_text('модель или чат не выбраны.', reply_markup=get_inline_buttons([
            [InlineKeyboardButton("сменить модель", callback_data='change_model')]
        ]))
        return

    model = model_id_to_info.get(model_id, {})
    model_type = model.get('type', 'text').lower()

    # Пробуем провайдеров по очереди
    providers = model_providers.get(model_id, [])
    answer = None
    for provider in providers:
        # Ищем переопределение model_id для провайдера
        model_id_override = config.get('model_id_overrides', {}).get(provider, {}).get(model_id)
        api_model_id = model_id_override or model_id

        context_store.setdefault(user_id, {}).setdefault(model_id, {}).setdefault(slot, load_chat_context(user_id, model_id, slot, SYSTEM_INSTRUCTION_TEXT))

        if model_type == 'image':
            has_cyrillic = bool(re.search('[а-яА-Я]', user_text))
            settings = user_settings_store.get(str(user_id), {"autotranslation": True, "autoimprovement": True})
            if has_cyrillic and (settings["autotranslation"] or settings["autoimprovement"]):
                await update.message.reply_text(
                    "картинка будет генерироваться чуть дольше. другой модели нужно перевести текст запроса на английский или улучшить его (от тебя не требуется никаких действий). это повысит шанс на то, что картинка получится удачной."
                )
                if settings["autotranslation"] and settings["autoimprovement"]:
                    improved = await translate_improve_text('translate_improve', user_text, user_id, context)
                elif settings["autotranslation"]:
                    improved = await translate_improve_text('translate', user_text, user_id, context)
                elif settings["autoimprovement"]:
                    improved = await translate_improve_text('improve', user_text, user_id, context)
                else:
                    improved = user_text

                if improved:
                    user_text = improved
                else:
                    await update.message.reply_text("не получилось перевести и улучшить текст.")
                    return
            context_store[user_id][model_id][slot].append({"role": "user", "content": user_text})
        elif model_type == 'multimodal':
            context_store[user_id][model_id][slot].append({"role": "user", "content": user_text})
        else:
            context_store[user_id][model_id][slot].append({"role": "user", "content": user_text})

        save_chat_context(user_id, model_id, slot, context_store[user_id][model_id][slot])

        answer = await get_model_response(provider, api_model_id, context_store[user_id][model_id][slot], model_type, user_id, user_text)

        if answer:
            check_and_increment_limit(user_id, model_id)
            break

    if answer:
        context_store[user_id][model_id][slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, model_id, slot, context_store[user_id][model_id][slot])
        mode = user_mode_store.get(str(user_id), 'message')

        if model_type != 'image':
            if mode == 'message':
                for msg in split_message(markdown_to_html(answer), MAX_MESSAGE_LENGTH):
                    await update.message.reply_text(msg, reply_markup=get_inline_buttons([
                        [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                            InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                            InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                        [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                    ]))

            elif mode == 'file':
                path = save_md(answer, user_id)
                if path:
                    try:
                        with open(path, 'rb') as f:
                            await update.message.reply_document(
                                document=InputFile(f),
                                filename=os.path.basename(path),
                                caption="твой ответ модели в формате markdown.",
                                reply_markup=get_inline_buttons([
                                    [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                                        InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                                        InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                                    [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                                ])
                            )
                        os.remove(path)
                    except Exception as e:
                        logger.error(f"Ошибка отправки файла: {e}")
                        await update.message.reply_text('не получилось отправить файл.')
                else:
                    await update.message.reply_text('не получилось создать файл с ответом.')

        else:
            try:
                await update.message.reply_photo(photo=answer, caption="твоя картинка:", reply_markup=get_inline_buttons([
                    [InlineKeyboardButton("сменить модель", callback_data='change_model'),
                        InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
                        InlineKeyboardButton("очистить контекст", callback_data='clear_context')],
                    [InlineKeyboardButton("продолжить ответ", callback_data='continue')]
                ]))
            except TelegramError as e:
                logger.error(f"Ошибка отправки изображения: {e}")
                await update.message.reply_text("Не удалось отправить изображение.")
    else:
        await update.message.reply_text(
            '<b>ни один провайдер не смог обработать запрос для выбранной модели.</b>\nтакое ОЧЕНЬ маловероятно. попробуй: очистить контекст; прописать /start; сменить модель.',
            parse_mode=ParseMode.HTML,
            reply_markup=get_inline_buttons([
                [InlineKeyboardButton("сменить модель", callback_data='change_model')]
            ])
        )
        
async def setmode(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Команда", f"/setmode {context.args}")
    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
    log_action(user_id, "Команда", "/mode")
    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
    log_action(user_id, "Команда", "/limits")
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("тебе сюда мяукать нельзя.")
        return

    user_limits = config.get('user_limits', {}).get(str(user_id), {})
    model_limits = config.get('model_limits', {})
    lines = ["<b>твои лимиты:</b>"]

    for model_id, limits in model_limits.items():
        model_name = model_id_to_name.get(model_id, model_id)  # Получаем имя модели или используем ID, если имя не найдено
        total_requests_key = f'{model_id}_total_requests'
        user_requests_key = f'{model_id}_requests'

        total_used = usage_store.get(total_requests_key, 0)
        total_limit = limits['total_daily_requests']
        total_remaining = total_limit - total_used

        user_limit = user_limits.get(model_id, {}).get("per_user_daily_requests", "infinite")
        user_used = usage_store.get('users', {}).get(str(user_id), {}).get(user_requests_key, 0)
        user_display = "∞" if user_limit == "infinite" else f"{user_limit}"
        user_remaining = "∞" if user_limit == "infinite" else f"{max(user_limit - user_used, 0)}"

        lines.append(f"\n- <b>{model_name}</b>:")
        lines.append(f"  общий лимит: {total_used} / {total_limit} (осталось: {total_remaining})")
        lines.append(f"  твой лимит: {user_used} / {user_display} (осталось: {user_remaining})")

    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
    log_action(user_id, "Команда", "/start")
    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}
    await update.message.reply_text('привет! выбери семейство моделей:', reply_markup=get_model_family_buttons())

async def reload_bot(update: Update, context: ContextTypes.DEFAULT_TYPE):
    admin_ids = {5005816138}
    user_id = update.effective_user.id
    log_action(user_id, "Команда", "/reload")
    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, custom_prompts, user_settings_store, model_providers
        config = load_json('config.json', default=config)
        AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))
        # Изменено: model_id_to_info теперь хранит только name и type
        model_id_to_info = {model_id: {'name': model_data.get('name', model_id), 'type': model_data.get('type', 'text')} for model_id, model_data in config.get('models', {}).items()}
        model_id_to_name = {info['id']: name for name, info in config.get('models', {}).items() if 'id' in info}
        
        RESTRICTED_MODELS = set() # Убрано
        
        AUTHORIZED_MODEL_USERS = {5005816138, 1759650343, 7856892361, 488179251, 1110920711}
        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)
        custom_prompts = load_json('custom_prompts.json', default=custom_prompts)
        user_settings_store = load_json('user_settings.json', default=user_settings_store)
        
        # model_providers теперь в config.json
        model_providers = config.get('model_providers', {})
        
        await update.message.reply_text('конфиг успешно перезагружен.')
        logger.info("конфиг бота успешно перезагружен.")

    except Exception as e:
        logger.error(f"Ошибка при перезагрузке конфигурации: {e}")
        await update.message.reply_text('произошла ошибка при перезагрузке конфига.')

async def custom_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Команда", f"/custom {context.args}")
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return
    if not context.args:
        await update.message.reply_text('использование: /custom твой промпт')
        return
    custom_prompts[str(user_id)] = ' '.join(context.args)
    save_json('custom_prompts.json', custom_prompts)
    await update.message.reply_text('твой системный промпт успешно сохранён. он не применяется к моделям o1 и моделям генерации изображений.', parse_mode=ParseMode.HTML)

async def clearcustom_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Команда", "/clearcustom")
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    if str(user_id) in custom_prompts:
        del custom_prompts[str(user_id)]
        save_json('custom_prompts.json', custom_prompts)
        await update.message.reply_text('твой собственный системный промпт очищен.', parse_mode=ParseMode.HTML)
    else:
        await update.message.reply_text('у тебя нет установленного собственного системного промпта.', parse_mode=ParseMode.HTML)



# Описания моделей
MODEL_DESCRIPTIONS = {
    "claude-3-5-sonnet-20241022": "передовая модель Anthropic. быстрая, в задачах с кодом в чём-то лучше даже OpenAI o1-mini и preview. актуальность знаний: до апреля 2024. контекстное окно: 200000 токенов. выплюнуть за раз может: 8192 токена",
    "claude-3-opus": "бывшая передовая модель Anthropic. медленная, но очень хороша в генерации текстов и удивляет своими познаниями русского языка. актуальность знаний: до августа 2023. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "claude-3.5-haiku": "почти передовая модель Anthropic. совсем новая, быстрая, лучше чем предшественник Claude 3 Haiku. актуальность знаний: неизвестна. контекстное окно: 200000 токенов. выплюнуть за раз может: 8192 токена.",
    "searchgpt": "реплика на новую модель OpenAI. имеет доступ в интернет. актуальность данных: онлайн, контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "chatgpt-4o-latest": "новейшая версия модели GPT-4o за ноябрь 2024, созданная OpenAI. быстрая, хороша для широкого спектра задач. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "gpt-4o": "новейшая версия модели 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": "передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 200000 токенов. выплюнуть за раз может: 100000 токенов.",
    "o1-preview": "почти передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов.",
    "o1-mini": "уменьшенная версия модели o1 от OpenAI. тоже использует цепочку рассуждений, но думает намного быстрее. может уступать o1-preview в качестве ответов, но это компенсируется её на 80% более низкой ценой (для тех кто платит за неё) и быстротой ответов. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов.",
    "flux.1.1-pro-ultra": "как Flux 1.1 Pro, но с х4 большим качеством картинок! некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "FLUX 1.1 [pro] ultra raw": "как Flux 1.1 Pro, но с х4 большим качеством картинок! сделана так чтобы в картинках было меньше синтетики и больше реальности. некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "FLUX.1.1 [pro]": "лучшая существующая модель генерации изображений. некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "flux.1.1-pro": "почти лучшая модель генерации изображений. очень нестабильна. можешь пробовать отправлять запрос несколько раз.",
    "FLUX.1 [pro]": "хорошая модель генерации изображений. очень нестабильна. можешь пробовать отправлять запрос несколько раз.",
    "stable-diffusion-3.5-large": "большая модель для генерации изображений по текстовому описанию.",
    "stable-diffusion-3.5-turbo": "быстрая модель для генерации изображений по текстовому описанию.",
    "dall-e-3-v2": "передовая модель генерации картинок OpenAI.",
    "recraft-v3": "хорошая модель генерации картинок. хорошо справляется с написанием текста на картинках!",
    "gemini-2.0-flash-exp": "передовая модель Google. очень быстрая и умная! а ещё мультимодальна. контекстное окно: 1048576 токенов. выплюнуть за раз может: 8192 токена",
    "gemini-exp-1121": "экспериментальная модель Google. хорошо рассуждает и пишет код! актуальность знаний: до июня 2024. контекстное окно: 32768 токенов. выплюнуть за раз может: 8192 токена.",
    "gemini-exp-1206": "экспериментальная модель Google. передовая на данный момент! актуальность знаний: до июня 2024. контекстное окно: 2097152 токена. выплюнуть за раз может: 8192 токена.",
    "learnlm-1.5-pro-experimental": "экспериментальная модель Google на основе Gemini 1.5 Pro, специально созданная для задач обучения и образования. может использоваться для генерации учебных материалов, объяснений и ответов на вопросы. актуальность знаний: до декабря 2023. контекстное окно: 32768 токенов. выплюнуть за раз может: 8192 токена.",
    "gemini-1.5-pro": "хорошая моделька от Google, но лучше использовать более новые Gemini 2.0 Flash и экспериментальные модели Google. актуальность знаний: до декабря 2023. контекстное окно: 2000000 токенов. выплюнуть за раз может: 8192 токена.",
    "gemini-1.0-pro": "старая моделька от Google. лучше используй более новые.",
    "claude-3.5-sonnet": "почти передовая модель Anthropic. быстрая и умная ( версия - за июнь 2024 ). контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "hermes-3-llama-3.1-405b": "большая текстовая моделька. Актуальность знаний: до декабря 2023 г. Контекстное окно: 128000 токенов. Выплюнуть за раз может: неизвестно.",
    "mistral-large-latest": "большая языковая модель от компании Mistral AI. Подходит для широкого спектра задач, включая генерацию текста, перевод и ответы на вопросы. Актуальность знаний: до февраля 2024 г. Контекстное окно: 32000 токенов. Выплюнуть за раз может: неизвестно.",
    "claude-instant-1.2": "устаревшая модель от Anthropic, оптимизированная для быстрого ответа. Подходит для задач, где важна скорость, но не требуется высокая точность. Актуальность знаний: до марта 2023 г. Контекстное окно: 100000 токенов. Выплюнуть за раз может: неизвестно.",
    "claude-2.1": "устаревшая модель от Anthropic, предшественник Claude 3. Подходит для широкого спектра задач, но уступает по производительности более новым моделям. Актуальность знаний: до марта 2023 г. Контекстное окно: 200000 токенов. Выплюнуть за раз может: неизвестно.",
    "llama-3.3-70b-versatile": "одна из новейших моделей семейства LLaMA. Отличается высокой производительностью и подходит для широкого спектра задач. Актуальность знаний: до марта 2024 г. Контекстное окно: 8192 токенов. Выплюнуть за раз может: неизвестно.",
    "llama-3.1-70b-turbo": "новая модель семейства LLaMA. Отличается высокой производительностью и подходит для широкого спектра задач. Актуальность знаний: до декабря 2023 г. Контекстное окно: 8192 токенов. Выплюнуть за раз может: неизвестно."
}

MODEL_SHORT_DESCRIPTIONS = {
    "claude-3-5-sonnet-20241022": "быстрая, хорошая и передовая модель Anthropic. многие считают её лучшей в написании кода, но она хороша и в других задачах, например генерации текста/сочинений.",
    "claude-3-opus": "бывшая передовая модель Anthropic. медленная, старая, но хорошо общается и прекрасно знает русский.",
    "claude-3.5-haiku": "одна из новых моделей Anthropic. маленькая, быстрая, но менее умная. лучше используй Claude 3.5 Sonnet.",
    "searchgpt": "GPT-4 Turbo с доступом в интернет. не задавай ей сложных запросов типа 'подробные новости за этот месяц'. ",
    "chatgpt-4o-latest": "новейшая версия GPT-4o от OpenAI. быстрая и очень креативная, в остальном тоже хороша.",
    "gpt-4o": "новая модель от OpenAI. обладает улучшенной производительностью по сравнению с предыдущими версиями.",
    "gpt-4-turbo": "более старая передовая модель OpenAI. медленнее GPT-4o, но порой точнее.\n<b>если тебе нужна эта модель - используй через провайдера Rimunace, не тут.</b>",
    "gpt-4o-mini": "очень быстрая! но уступает другим моделям в качестве ответов.\n<b>лучше вообще пользуйся другими моделями этого провайдера, лимиты-то общие для всех его моделей, кроме моделей генерации картинок.</b>",
    "o1": "передовая моделька OpenAI. рассуждает при создании ответов. прекрасна в сложных задачах типа решения задач по физике/математике, отладки кода, и прочем, но не особо креативная.",
    "o1-preview": "почти передовая моделька OpenAI. рассуждает при создании ответов. прекрасна в сложных задачах типа решения задач по физике/математике, отладки кода, и прочем, но не особо креативная.",
    "o1-mini": "уменьшенная версия o1 и тоже рассуждает при создании ответов. прекрасна в написании кода (почти полностью бот написан ей), а в остальном уступает o1-preview. имеет довольно ограниченные знания о мире.",
    "flux.1.1-pro-ultra": "лучшая модель генерации картинок. выдаёт х4 качества относительно Flux 1.1 Pro. очень нестабильна. можешь пробовать отправлять запрос несколько раз.",
    "FLUX 1.1 [pro] ultra raw": "лучшая модель генерации картинок, делает более реалистично. выдаёт х4 качества относительно Flux 1.1 Pro. очень нестабильна. можешь пробовать отправлять запрос несколько раз.",
    "FLUX.1.1 [pro]": "почти лучшая модель генерации картинок. очень нестабильна. можешь пробовать отправлять запрос несколько раз.",
    "flux.1.1-pro": "почти лучшая модель генерации картинок. очень нестабильна. можешь пробовать отправлять запрос несколько раз.",
    "FLUX.1 [pro]": "хорошая модель генерации картинок. очень нестабильна. можешь пробовать отправлять запрос несколько раз.",
    "stable-diffusion-3.5-turbo": "быстрая моделька генерации картинок. вообще, лучше используй модели Flux.",
    "stable-diffusion-3.5-large": "большая моделька генерации картинок. вообще, лучше используй модели Flux.",
    "dall-e-3-v2": "передовая модель генерации картинок OpenAI.",
    "recraft-v3": "хорошая модель генерации картинок. хорошо управляется с написанием текста на картинках.",
    "gemini-2.0-flash-exp": "экспериментальная модель от Google, оптимизированная для скорости. Подходит для задач, где важна скорость ответа.",
    "gemini-exp-1121": "экспериментальная модель от Google. хороша в рассуждении.",
    "gemini-exp-1206": "экспериментальная модель от Google. лучшая из существующих моделей.",
    "learnlm-1.5-pro-experimental": "экспериментальная модель от Google, созданная для образовательных задач.",
    "gemini-1.5-pro": "новая модель от Google, пришедшая на смену Gemini 1.0 Pro. Улучшенная производительность и расширенные возможности.",
    "gemini-1.0-pro": "предыдущая версия модели от Google. Может использоваться для различных задач, но уступает по возможностям более новым моделям.",
    "claude-3.5-sonnet": "новая модель от Anthropic. Быстрая, мощная, отлично подходит для написания кода и других задач.",
    "hermes-3-llama-3.1-405b": "модель на основе LLaMA 3.1, дообученная для улучшения производительности. Подходит для различных задач.",
    "mistral-large-latest": "большая языковая модель от Mistral AI. Подходит для широкого круга задач.",
    "claude-instant-1.2": "старая, но быстрая модель от Anthropic. Подходит для задач, где важна скорость, а не точность.",
    "claude-2.1": "более старая версия модели от Anthropic. Уступает по возможностям Claude 3, но всё ещё может быть полезна.",
    "llama-3.3-70b-versatile": "моделька, по производительности схожая с LLaMA 3.1 405b. то есть в 5 раз большей моделью!",
    "llama-3.1-70b-turbo": "быстрая, но не самая актуальная моделька."
}

async def models_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Команда", "/models")
    if not model_id_to_info:
        await update.message.reply_text('нет доступных моделей.', reply_markup=get_inline_buttons([
            [InlineKeyboardButton("сменить модель", callback_data='change_model')]
        ]))
        return
    # model_id as is, только берём name
    descriptions = [
        f"- <b>{html.escape(model_id_to_name.get(mid, 'неизвестная модель'))}</b>: {html.escape(desc)}"
        for mid, desc in MODEL_DESCRIPTIONS.items() if mid in model_id_to_info
    ]
    for i in range(0, len(descriptions), 10):
        chunk = descriptions[i:i+10]
        await update.message.reply_text("<b>описание доступных моделей:</b>\n\n" + "\n".join(chunk), parse_mode=ParseMode.HTML)

async def meowlogs_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Отправляет .md файл с логами администратору."""
    user_id = update.effective_user.id
    log_action(user_id, "Команда", update.message.text)

    if user_id != 5005816138:
        return

    try:
        if context.args:
            period = int(context.args[0])
            if period <= 0:
                raise ValueError
        else:
            period = None
    except (ValueError, IndexError):
        await update.message.reply_text("Использование: `.meowlogs [период в минутах, больше 0]`")
        return

    md_file_path = await generate_md_log(period)

    if md_file_path:
        try:
            async with aiofiles.open(md_file_path, "rb") as md_file:
                file_content = await md_file.read()
                await context.bot.send_document(
                    chat_id=user_id,
                    document=TelegramInputFile(BytesIO(file_content), filename=os.path.basename(md_file_path)),
                    caption=f"логи бота за {'последние {period} минут' if period else 'всё время'}.",   
                )
            os.remove(md_file_path)
        except Exception as e:
            logger.error(f"Ошибка при отправке файла лога: {e}")
            await update.message.reply_text("Произошла ошибка при отправке логов.")
    else:
        await update.message.reply_text("Файл лога не найден или произошла ошибка при его создании.")

async def meowsend_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Отправляет сообщение от имени бота указанному пользователю."""
    user_id = update.effective_user.id
    log_action(user_id, "Команда", update.message.text)

    if user_id != 5005816138:
        return

    args_str = update.message.text.replace(".meowsend ", "")
    args = args_str.split()

    logger.info(f"Аргументы команды .meowsend: {args}")

    if not args or len(args) < 2:
        await update.message.reply_text("Использование: `.meowsend user_id message`")
        return

    try:
        target_user_id = int(args[0])
        message = " ".join(args[1:])

        if not message:
            raise ValueError("Сообщение не может быть пустым.")

        logger.info(f"Отправка сообщения '{message}' пользователю {target_user_id}")

        await context.bot.send_message(chat_id=target_user_id, text=message)
        await update.message.reply_text(f"Сообщение отправлено пользователю {target_user_id}.")

    except ValueError as e:
        logger.error(f"ValueError: {e}")
        await update.message.reply_text(f"Ошибка: {e}")
    except IndexError as e:
        logger.error(f"IndexError: {e}")
        await update.message.reply_text("Использование: `.meowsend user_id message`")
    except TelegramError as e:
        logger.error(f"TelegramError: {e}")
        await update.message.reply_text(f"Ошибка при отправке сообщения пользователю {target_user_id}: {e}")

async def instructions(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    log_action(user_id, "Команда", "/instructions")
    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"
        "<b>/custom промпт</b> - установить твой собственный системный промпт.\n"
        "<b>/clearcustom</b> - очистить твой собственный системный промпт.\n\n"
        "кнопки:\n"
        "<b>сменить модель</b> - выбрать другую модель.\n"
        "<b>выбрать чат</b> - выбрать другой чат для общения с моделью.\n"
        "<b>очистить контекст</b> - полностью очищает контекстное окно модели.\n"
        "<b>продолжить ответ</b> - продолжить ответ модели.\n"
        "‼️<b>токены:</b> токен не равен символу. все модели воспринимают его по разному, будь то слово, кусок слова или вообще символ.\n"
        "‼️<b>лимиты бота:</b> подробно описаны в /limits.\n"
        "‼️<b>контекст:</b> 4 чата на каждую модель. если хочешь поговорить с моделью на другую тему или эта тема для тебя уже не актуальна - очисти контекст, так будет тратиться значительно меньше токенов."
    )
    await update.message.reply_text(menu_text, parse_mode=ParseMode.HTML)

# Обработчик ошибок
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
            model_id = user_state.get(user_id, {}).get('current_model_id')
            await update.effective_message.reply_text(
                'модель не выдала ответ. проблема на стороне провайдера. попробуй отправить запрос ещё раз или смени модель.',
                reply_markup=get_inline_buttons([
                    [InlineKeyboardButton("сменить модель", callback_data='change_model')]
                ]) if model_id else None
            )
        except TelegramError as e:
            logger.error(f"Не удалось отправить сообщение об ошибке пользователю: {e}")

# Фоновая задача для сброса лимитов
async def reset_limits_daily(application):
    while True:
        now = datetime.now(ZoneInfo('Europe/Moscow'))
        reset_hour, reset_minute = map(int, config.get('limit_reset_time', '10:00').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)

        try:
            # Сброс глобальных счетчиков
            for model_id in config.get('model_limits', {}):
                total_requests_key = f'{model_id}_total_requests'
                usage_store[total_requests_key] = 0

            # Сброс пользовательских счетчиков
            for user_id, limits in config.get('user_limits', {}).items():
                for model_id, model_user_limits in limits.items():
                    if model_user_limits.get("per_user_daily_requests", "infinite") != "infinite":
                        user_requests_key = f'{model_id}_requests'
                        usage_store.setdefault('users', {}).setdefault(user_id, {}).update({user_requests_key: 0})

            # Сброс счетчиков для провайдеров с типом учета "tokens" (например, cablyai)
            for provider_id, provider_data in config.get('providers', {}).items():
                if provider_data.get('unit_type') == 'tokens':
                    usage_store[f'{provider_id}_tokens'] = 0

            # Сброс счетчика для провайдера nobrandai
            usage_store['nobrandai_credits'] = min(usage_store.get('nobrandai_credits', 0) + 500, 11000)

            save_json('usage.json', usage_store)

        except Exception as e:
            logger.error(f"Ошибка при сбросе лимитов: {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))
    application.add_handler(CommandHandler("custom", custom_command))
    application.add_handler(CommandHandler("clearcustom", clearcustom_command))
    application.add_handler(CommandHandler("instructions", instructions))
    application.add_handler(CommandHandler("settings", settings_command))
    application.add_handler(MessageHandler(filters.Regex(r"^\.meowlogs(\s+\d+)?$"), meowlogs_command))
    application.add_handler(MessageHandler(filters.Regex(r"^\.meowsend\b(.*)"), meowsend_command))
    # Обработчики CallbackQuery
    application.add_handler(CallbackQueryHandler(handle_callback_query, pattern=(
    '^(toggle_autotranslation|toggle_autoimprovement|model_select:.+|chat_slot:\d+:.+|change_model|'
    'back_to_families|change_chat_slot|clear_context|continue|next_chats|back_chats)$'
)))

    # Изменено: обработчик выбора семейства моделей вместо провайдеров
    # Изменено: обновлено регулярное выражение для учёта эмодзи
    application.add_handler(MessageHandler(
        filters.Regex('^('
                        '⚡️ модели GPT|'
                        '🧠 модели Gemini|'
                        '☁️ модели Claude|'
                        '💥 модели o1|'
                        '🦙 модели LLaMA|'
                        '🔀 другие модели|'
                        '🖼️ модели для картинок)$') & filters.TEXT, handle_model_family_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(allowed_updates=Update.ALL_TYPES)

if __name__ == '__main__':
    main()