привет! вот код моего телеграм бота для доступа к моделям типа тебя. я хочу добавить туда альтернативный нынешнему api - официальный api OpenAI. я возьму оттуда следующие модели: chatgpt-4o-latest, gpt-4-turbo, gpt-4o-mini, o1-preview, o1-mini. первые 3 мультимодальны, последние 2 нет. интегрируй это в мой код, руководствуясь OpenAI API сделай так чтобы пользователи могли отправлять изображения указанным мультимодальным моделям и чтобы эти модели могли их обрабатывать. и сделай так чтобы пользователи бота могли выбирать новые модели, нажимая кнопку, как сделано по аналогии с уже существующими моделями. после кода бота будет код config.json, измени его тоже, чтобы модели от OpenAI корректно интегрировались. похожие в том файле уже есть, но они от уже существующего API, поэтому предлагаю тебе новые добавленные модели назвать как-то немного иначе. код бота:
import logging
import requests
import json
import os
import re
import html
from datetime import datetime
from telegram import (
    Update,
    InlineKeyboardMarkup,
    InlineKeyboardButton,
    InputFile
)
from telegram.constants import ParseMode
from telegram.ext import (
    ApplicationBuilder,
    CommandHandler,
    MessageHandler,
    ContextTypes,
    filters,
    CallbackQueryHandler,
    Defaults,
)
from telegram.error import TelegramError

# Функция для загрузки конфигурации
def load_config():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(script_dir, 'config.json')
    
    if not os.path.exists(config_path):
        raise FileNotFoundError(f"Файл config.json не найден по пути: {config_path}")
    
    with open(config_path, 'r', encoding='utf-8') as f:
        return json.load(f)

# Загрузка конфигурации
try:
    config = load_config()
except FileNotFoundError as e:
    print(e)
    exit(1)
except json.JSONDecodeError as e:
    print(f"Ошибка разбора config.json: {e}")
    exit(1)

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

# Ваши API-эндпоинты
API_URL_TEXT = 'https://cablyai.com/v1/chat/completions'
API_URL_IMAGE = 'https://cablyai.com/v1/images/generations'  # Используем ваш актуальный URL для генерации изображений
API_HEADERS = {
    'Authorization': f"Bearer {config['api_key']}",
    'Content-Type': 'application/json'
}

# Инструкция для текстовых моделей
SYSTEM_INSTRUCTION_TEXT = config.get('system_instruction', "ты ведёшь себя как котик")

# Хранение контекста для каждой сессии и модели
context_store = {}
model_store = {}
MAX_MESSAGE_LENGTH = 4096
# Преобразуем список авторизованных пользователей в множество для быстрого поиска
AUTHORIZED_USER_IDS = set(config.get('authorized_user_ids', []))

# Хранение режима пользователя ('message' или 'file')
user_mode_store = {}

# Создание маппинга model_id -> model_info для быстрого доступа
model_id_to_info = {}
model_id_to_name = {}
for model_name, model_info in config.get('models', {}).items():
    model_id = model_info.get('id')
    if model_id:
        model_id_to_info[model_id] = model_info
        model_id_to_name[model_id] = model_name
    else:
        logger.warning(f"Модель '{model_name}' не имеет 'id'.")

# Список ограниченных моделей
RESTRICTED_MODELS = ['o1-preview', 'o1-mini']
AUTHORIZED_MODEL_USER = 5005816138  # ID пользователя, которому разрешены ограниченные модели

# Описания моделей (примерные, необходимо доработать)
MODEL_DESCRIPTIONS = {
    "claude-3-5-sonnet-20241022": "⚡️Claude 3.5 Sonnet — передовая модель Anthropic. быстрая, в задачах с кодом в чём-то лучше даже OpenAI o1-mini и preview. актуальность знаний: до апреля 2024. контекстное окно: 200000 токенов. выплюнуть за раз может: 5000 символов ( символ не равно токен ).",
    "claude-3-opus": "Claude 3 Opus — бывшая передовая модель Anthropic. медленная, но очень хороша в генерации текстов и удивляет своими познаниями русского языка. актуальность знаний: до августа 2023. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "chatgpt-4o-latest": "⚡️GPT-4o-latest — новейшая версия модели GPT-4o, созданная OpenAI. быстрая, хороша для широкого спектра задач. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "gpt-4-turbo": "GPT-4 Turbo — бывшая передовая модель OpenAI. иногда в чём-то превосходит даже GPT-4o-latest, но уступает в скорости. актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 4096 токенов.",
    "gpt-4o-mini": "GPT-4o mini — уменьшенная версия GPT-4o. очень быстрая! но по качеству ответов уступает. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "o1-preview": "💥o1-preview — передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер, но генерирует ответ довольно долго относительно других моделей. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов.",
    "o1-mini": "💥o1-mini — уменьшенная версия модели o1 от OpenAI. тоже использует цепочку рассуждений, но думает намного быстрее. может уступать o1-preview в качестве ответов, но это компенсируется её на 80% более низкой ценой (для тех кто платит за неё) и быстротой ответов. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 65536 токенов.",
    "llama-3.1-405b-turbo": "LLaMA 3.1 405b — текстовая модель. сопоставима по качеству ответов с остальными моделями, но может уступать, и выдавать всякие иероглифы посреди ответа. но почему бы и не добавить? актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: неизвестно. ",
    "FLUX.1.1 [pro]": "✨Flux 1.1 Pro — лучшая существующая модель генерации изображений. некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "sd3.5": "Stable Diffusion 3.5 — модель для генерации изображений по текстовому описанию. контекста не имеет, работая лишь с текущим заданным запросом."
}

# Функция для разбивки длинного сообщения на части
def split_message(message, max_length):
    return [message[i:i + max_length] for i in range(0, len(message), max_length)]

# Функция для создания кнопок выбора модели
def get_model_buttons(user_id):
    buttons = []
    for name, model_info in config.get('models', {}).items():
        model_id = model_info['id']
        if model_id in RESTRICTED_MODELS and user_id != AUTHORIZED_MODEL_USER:
            # Добавляем кнопку, но доступ к модели ограничен
            buttons.append([InlineKeyboardButton(html.escape(name.lower()), callback_data=f"restricted:{model_id}")])
        else:
            buttons.append([InlineKeyboardButton(html.escape(name.lower()), callback_data=model_id)])
    return InlineKeyboardMarkup(buttons)

# Функция для создания основных кнопок: 'сменить модель' и 'продолжить'
def get_main_buttons():
    keyboard = [
        [
            InlineKeyboardButton("сменить модель", callback_data='change_model'),
            InlineKeyboardButton("продолжить", callback_data='continue_response')
        ]
    ]
    return InlineKeyboardMarkup(keyboard)

# Определение get_menu_buttons как алиаса для get_main_buttons
def get_menu_buttons():
    return get_main_buttons()

# Функция для определения, является ли текст русским
def is_russian(text):
    return bool(re.search('[а-яА-Я]', text))

# Функция для перевода текста с помощью модели GPT-4
def translate_to_english(original_text):
    translation_prompt = f"переведи следующий текст на английский язык и верни только переведенный текст:\n\n{original_text}"
    
    translation_data = {
        "model": "chatgpt-4o-latest",
        "messages": [
            {"role": "system", "content": "you are a helpful assistant that translates russian text to english."},
            {"role": "user", "content": translation_prompt}
        ]
    }
    
    try:
        response = requests.post(API_URL_TEXT, headers=API_HEADERS, json=translation_data)
        response.raise_for_status()
        response_data = response.json()
        
        if "choices" in response_data and len(response_data["choices"]) > 0:
            translated_text = response_data["choices"][0].get('message', {}).get('content', '').strip()
            return translated_text
        else:
            logger.error("Неверная структура ответа от модели при переводе.")
            return None
    except requests.exceptions.RequestException as e:
        logger.error(f"Ошибка при переводе текста: {e}")
        return None

# Функция для преобразования Markdown в HTML
def markdown_to_html(text):
    """
    Простой конвертер Markdown в HTML.
    Обрабатывает заголовки, **жирный**, *курсив*, __подчёркивание__, ~~зачёркивание~~.
    """
    # Экранирование HTML-символов
    text = html.escape(text)
    
    # Замена заголовков: ### Заголовок -> <b>Заголовок</b>
    for i in range(6, 0, -1):  # Обрабатываем заголовки от h6 до h1
        pattern = r'^{0} (.+)$'.format('#' * i)
        replacement = r'<b>\1</b>'
        text = re.sub(pattern, replacement, text, flags=re.MULTILINE)
    
    # Замена **жирный** на <b>жирный</b>
    text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text)
    
    # Замена *курсив* на <i>курсив</i>
    text = re.sub(r'\*(.*?)\*', r'<i>\1</i>', text)
    
    # Замена __подчёркивание__ на <u>подчёркивание</u>
    text = re.sub(r'__(.*?)__', r'<u>\1</u>', text)
    
    # Замена ~~зачёркивание~~ на 
зачёркивание

    text = re.sub(r'~~(.*?)~~', r'
\1
', text)
    
    return text

# Функция для сохранения ответа в .md файл
def save_md_file(content, user_id):
    """
    Сохраняет содержимое в .md файл с уникальным именем.
    Возвращает путь к файлу.
    """
    if not os.path.exists('responses'):
        os.makedirs('responses')
    
    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    filename = f"responses/{user_id}_{timestamp}.md"
    
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(content)
        return filename
    except Exception as e:
        logger.error(f"Ошибка при сохранении файла .md: {e}")
        return None

# Обработчик команды /set_mode
async def set_mode(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Позволяет пользователю выбрать режим получения ответов:
    - message: получение текстовых сообщений
    - file: получение ответов в .md файлах
    """
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /set_mode: {user_id}")
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return
    
    if len(context.args) != 1:
        await update.message.reply_text('использование: /set_mode [message|file]')
        return
    
    mode = context.args[0].lower()
    if mode not in ['message', 'file']:
        await update.message.reply_text('неверный режим. используй "message" или "file".')
        return
    
    user_mode_store[user_id] = mode
    await update.message.reply_text(f'режим получения ответов установлен на: <b>{mode}</b>', parse_mode=ParseMode.HTML)

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

# Обработчик команды /instructions
async def instructions(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Отправляет пользователю инструкции по использованию бота.
    """
    menu_text = (
        "<b>инструкция по использованию бота:</b>\n\n"
        "<b>/start</b> - запустить бота и выбрать модель для взаимодействия.\n"
        "<b>/set_mode [message|file]</b> - выбрать режим получения ответов (message - текстовые сообщения, file - .md файлы, они нужны для того чтобы получить полный ответ модели без ограничения длины сообщения телеграма).\n"
        "<b>/get_mode</b> - узнать текущий выбранный режим.\n"
        "<b>/instructions</b> - получить инструкции по использованию бота.\n"
        "<b>/models</b> - получить описание доступных моделей.\n\n"
        "также доступно использование кнопок ниже:\n"
        "<b>сменить модель</b> - выбрать другую модель для общения.\n"
        "<b>продолжить</b> - продолжить ответ модели.\n"
        "‼️<b>важно:</b> токен не равно символ. все модели воспринимают токен как единицу измерения по разному, будь то слово, кусок слова или символ. если про модель указано что она отвечает определённым количеством символов - так и есть.\n"
        "‼️<b>лимиты бота:</b> если у тебя есть доступ к этому боту - лимитов никаких нет кроме как на o1 модели ( если тебе к ним открыт доступ ). админ бота не гарантирует что бот сможет работать бесконечно, всё зависит от провайдера api ключей."
    )
    
    # Отправляем инструкции как сообщение без кнопок
    await update.message.reply_text(menu_text, parse_mode=ParseMode.HTML)

# Обработчик команды /models
async def models_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Отправляет пользователю список доступных моделей с описаниями.
    """
    models = config.get('models', {})
    if not models:
        await update.message.reply_text('нет доступных моделей.', reply_markup=get_main_buttons())
        return
    
    message = "<b>описание доступных моделей:</b>\n\n"
    for name, info in models.items():
        model_id = info.get('id')
        model_name = name.lower()
        model_type = info.get('type', 'неизвестный тип').lower()
        description = MODEL_DESCRIPTIONS.get(model_id, "описание недоступно.")
        message += f"- <b>{model_name}</b> ({model_type}): {description}\n"
    
    await update.message.reply_text(message, parse_mode=ParseMode.HTML)

# Обработчик команды /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /start: {user_id}")
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return

    chat_id = update.effective_chat.id
    # Инициализация контекста для нового пользователя
    if chat_id not in context_store:
        context_store[chat_id] = {}
    
    # Если модель ещё не выбрана, устанавливаем модель по умолчанию
    if chat_id not in model_store:
        if model_id_to_info:
            default_model_id = next(iter(model_id_to_info))
            model_store[chat_id] = default_model_id
            model_info = model_id_to_info[default_model_id]
            if model_info.get('type') in ['text', 'multimodal']:
                if default_model_id not in context_store[chat_id]:
                    # Только для текстовых и мультимодальных моделей инициализируем контекст
                    if model_info.get('type') in ['text', 'multimodal']:
                        context_store[chat_id][default_model_id] = [{"role": "system", "content": SYSTEM_INSTRUCTION_TEXT}]
        else:
            await update.message.reply_text('нет доступных моделей для использования.', reply_markup=get_main_buttons())
            return
    
    # Кнопки для выбора модели
    if config.get('models'):
        await update.message.reply_text(
            'привет! выбери модель для общения:',
            reply_markup=get_model_buttons(user_id)
        )
    else:
        await update.message.reply_text(
            'привет!\n\nнет доступных моделей для выбора.',
            reply_markup=get_main_buttons()
        )

# Обработчик выбора модели через кнопки
async def select_model(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался выбрать модель: {user_id}")
        await query.edit_message_text('тебе сюда мяукать нельзя.')
        return

    selected_model_data = query.data

    # Проверка, является ли модель ограниченной
    if selected_model_data.startswith("restricted:"):
        model_id = selected_model_data.split("restricted:")[1]
        if user_id != AUTHORIZED_MODEL_USER:
            logger.warning(f"Пользователь {user_id} пытался использовать ограниченную модель {model_id}")
            await query.edit_message_text('вам нельзя использовать эту модель.')
            return
    else:
        model_id = selected_model_data

    model_info = model_id_to_info.get(model_id)

    if not model_info:
        logger.error(f"Выбранная модель не найдена в конфигурации: {model_id}")
        await query.edit_message_text('выбранная модель не найдена в конфигурации.')
        return

    chat_id = update.effective_chat.id
    model_store[chat_id] = model_id

    model_type = model_info.get('type', 'text')

    # Инициализация контекста для выбранной модели, только если это текстовая или мультимодальная модель
    if model_type in ['text', 'multimodal']:
        if model_id not in context_store.get(chat_id, {}):
            context_store[chat_id][model_id] = [{"role": "system", "content": SYSTEM_INSTRUCTION_TEXT}]
    else:
        # Для других типов моделей контекст не инициализируется или очищается
        if chat_id in context_store and model_id in context_store[chat_id]:
            del context_store[chat_id][model_id]

    model_name = html.escape(model_id_to_name.get(model_id, "Неизвестная модель"))
    await query.edit_message_text(f'модель изменена на: <b>{model_name}</b>', reply_markup=get_main_buttons())

# Обработчик текстовых сообщений
async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь отправил сообщение: {user_id}")
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return

    chat_id = update.effective_chat.id
    user_message = update.message.text

    # Проверка, нажата ли кнопка "меню"
    if user_message.lower() == "меню":
        await show_menu(update, context)
        return

    # Получаем режим пользователя, по умолчанию 'message'
    mode = user_mode_store.get(user_id, 'message')

    if chat_id not in model_store:
        await update.message.reply_text('модель не выбрана. используется модель по умолчанию.')
        if model_id_to_info:
            default_model_id = next(iter(model_id_to_info))
            model_store[chat_id] = default_model_id
            model_info = model_id_to_info[default_model_id]
            if model_info.get('type') in ['text', 'multimodal']:
                context_store[chat_id].setdefault(default_model_id, []).append({"role": "system", "content": SYSTEM_INSTRUCTION_TEXT})
        else:
            await update.message.reply_text('нет доступных моделей для использования.', reply_markup=get_main_buttons())
            return

    selected_model_id = model_store.get(chat_id)
    model_info = model_id_to_info.get(selected_model_id)
    if not model_info:
        logger.error(f"Модель не найдена в конфигурации для чата {chat_id}: {selected_model_id}")
        await update.message.reply_text('модель не найдена в конфигурации.', reply_markup=get_main_buttons())
        return

    selected_model_type = model_info.get('type', 'text')

    # Проверка поддержки текстового режима моделью
    if selected_model_type not in ['text', 'multimodal', 'image']:
        await update.message.reply_text('выбранная модель не поддерживает этот тип ввода.', reply_markup=get_menu_buttons())
        return

    # Проверка доступа к модели
    if selected_model_id in RESTRICTED_MODELS and user_id != AUTHORIZED_MODEL_USER:
        await update.message.reply_text('вам нельзя использовать эту модель.', reply_markup=get_menu_buttons())
        return

    # Инициализация контекста для текстовой или мультимодальной модели, если ещё не инициализирована
    if selected_model_type in ['text', 'multimodal'] and selected_model_id not in context_store.get(chat_id, {}):
        context_store[chat_id][selected_model_id] = [{"role": "system", "content": SYSTEM_INSTRUCTION_TEXT}]
    
    # Добавление сообщения пользователя в контекст для текстовых и мультимодальных моделей
    if selected_model_type in ['text', 'multimodal']:
        context_store[chat_id][selected_model_id].append({"role": "user", "content": user_message})

        # Подготовка данных для API с учетом контекста
        data = {
            "model": selected_model_id,
            "messages": context_store[chat_id][selected_model_id]
        }
        api_url = API_URL_TEXT
    elif selected_model_type == 'image':
        # Обработка для моделей типа 'image'
        if is_russian(user_message):
            # Сообщаем пользователю о переводе
            await update.message.reply_text('твой запрос содержит русский язык. генерация картинки будет идти чуть дольше - другой модели нужно перевести текст на английский для более корректного понимания моделью твоего запроса. если тебя не устроила полученная картинка - ты всегда можешь сделать ещё запрос.')

            # Переводим запрос на английский
            translated_prompt = translate_to_english(user_message)
            if not translated_prompt:
                await update.message.reply_text('не удалось перевести запрос. попробуй позже.', reply_markup=get_main_buttons())
                return

            # Используем переведенный запрос для генерации изображения
            data = {
                "model": selected_model_id,
                "prompt": translated_prompt  # Не экранируем, так как это отправка API
            }
        else:
            # Подготовка данных для API без перевода
            data = {
                "model": selected_model_id,
                "prompt": user_message  # Не экранируем, так как это отправка API
            }
        api_url = API_URL_IMAGE
    else:
        await update.message.reply_text('неизвестный тип модели.', reply_markup=get_main_buttons())
        return

    try:
        # Отправка запроса в API
        response = requests.post(api_url, headers=API_HEADERS, json=data)
        response.raise_for_status()  # Эта строка вызовет исключение для статусов ошибок
    except requests.exceptions.RequestException as e:
        logger.error(f"Ошибка запроса к API: {e}")
        await update.message.reply_text('api не отвечает или произошла ошибка при обработке запроса. попробуй позже.', reply_markup=get_main_buttons())
        return

    if response.status_code == 200:
        response_data = response.json()
        logger.info(f"Ответ API: {response_data}")

        if selected_model_type in ['text', 'multimodal']:
            # Проверка структуры ответа для текстовых моделей
            if "choices" in response_data and len(response_data["choices"]) > 0:
                answer = response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
                
                if mode == 'message':
                    # Преобразование Markdown в HTML
                    answer_html = markdown_to_html(answer)
                    
                    # Добавление ответа ассистента в контекст
                    context_store[chat_id][selected_model_id].append({"role": "assistant", "content": answer})
                    
                    # Разбивка длинного ответа на части и отправка
                    messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
                    for msg in messages:
                        await update.message.reply_text(msg, reply_markup=get_main_buttons())
                elif mode == 'file':
                    # Сохранение ответа в .md файл
                    file_path = save_md_file(answer, user_id)
                    if file_path:
                        # Отправка файла пользователю
                        try:
                            with open(file_path, 'rb') as f:
                                await update.message.reply_document(
                                    document=InputFile(f),
                                    filename=os.path.basename(file_path),
                                    caption="ваш ответ модели в формате markdown.",
                                    reply_markup=get_main_buttons()
                                )
                            
                            # Удаление файла после отправки
                            os.remove(file_path)
                        except Exception as e:
                            logger.error(f"Ошибка при отправке файла: {e}")
                            await update.message.reply_text('не удалось отправить файл. попробуйте позже.', reply_markup=get_main_buttons())
                    else:
                        await update.message.reply_text('не удалось создать файл с ответом.', reply_markup=get_main_buttons())
        elif selected_model_type == 'image':
            # Обработка ответа для image моделей
            image_data = response_data.get('data')
            if isinstance(image_data, list) and len(image_data) > 0:
                for image in image_data:
                    image_url = image.get('url')
                    if image_url:
                        # Отправка изображения пользователю
                        await update.message.reply_photo(photo=image_url, reply_markup=get_main_buttons())
                    else:
                        await update.message.reply_text('не удалось получить url изображения.', reply_markup=get_main_buttons())
            else:
                await update.message.reply_text('не удалось получить изображение. попробуй позже.', reply_markup=get_main_buttons())
    else:
        logger.error(f"Непредвиденный статус ответа API: {response.status_code}")
        await update.message.reply_text('api вернул непредвиденный ответ. попробуй позже.', reply_markup=get_main_buttons())

# Обработчик документов (файлов)
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался отправить файл: {user_id}")
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return

    chat_id = update.effective_chat.id
    document = update.message.document
    file = await document.get_file()
    filename = document.file_name.lower()  # Приводим к нижнему регистру для надёжной проверки

    # Проверка, является ли файл .py
    if filename.endswith('.py'):
        # Обработка как текстовый файл
        try:
            # Создаём директорию для скачивания, если её нет
            download_path = os.path.join('downloads', 'python_files')
            if not os.path.exists(download_path):
                os.makedirs(download_path)

            # Скачиваем файл
            file_path = os.path.join(download_path, document.file_name)
            await file.download_to_drive(file_path)

            # Читаем содержимое файла
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Удаляем файл после чтения
            os.remove(file_path)
        except Exception as e:
            logger.error(f"Ошибка при скачивании или чтении файла: {e}")
            await update.message.reply_text('не удалось скачать или прочитать файл. попробуй позже.', reply_markup=get_main_buttons())
            return

        # Получаем информацию о выбранной модели
        model_id = model_store.get(chat_id)
        model_info = model_id_to_info.get(model_id)
        if not model_info:
            logger.error(f"Модель не выбрана или не найдена для чата {chat_id}: {model_id}")
            await update.message.reply_text('модель не выбрана или не найдена.', reply_markup=get_main_buttons())
            return

        # Проверка доступа к модели
        if model_id in RESTRICTED_MODELS and user_id != AUTHORIZED_MODEL_USER:
            logger.warning(f"Пользователь {user_id} пытался использовать ограниченную модель {model_id}")
            await update.message.reply_text('вам нельзя использовать эту модель.', reply_markup=get_main_buttons())
            return

        # Проверяем, поддерживает ли модель текстовый ввод
        if model_info.get('type') not in ['text', 'multimodal', 'image']:
            await update.message.reply_text('выбранная модель не поддерживает обработку текстовых файлов.', reply_markup=get_main_buttons())
            return

        # Добавляем сообщение пользователя в контекст для текстовых и мультимодальных моделей
        if model_info.get('type') in ['text', 'multimodal']:
            context_store[chat_id][model_id].append({"role": "user", "content": content})

            # Подготовка данных для API с учетом контекста
            data = {
                "model": model_id,
                "messages": context_store[chat_id][model_id]
            }
            api_url = API_URL_TEXT
        elif model_info.get('type') == 'image':
            # Обработка для моделей типа 'image' (если требуется)
            await update.message.reply_text('обработка файлов для моделей типа "image" не поддерживается.', reply_markup=get_main_buttons())
            return
        else:
            await update.message.reply_text('неизвестный тип модели.', reply_markup=get_main_buttons())
            return

        try:
            # Отправка запроса в API
            response = requests.post(api_url, headers=API_HEADERS, json=data)
            response.raise_for_status()  # Эта строка вызовет исключение для статусов ошибок
        except requests.exceptions.RequestException as e:
            logger.error(f"Ошибка запроса к API: {e}")
            await update.message.reply_text('API не отвечает или произошла ошибка при обработке запроса. попробуй позже.', reply_markup=get_main_buttons())
            return

        if response.status_code == 200:
            response_data = response.json()
            logger.info(f"Ответ API: {response_data}")

            if model_info.get('type') in ['text', 'multimodal']:
                # Проверка структуры ответа для текстовых моделей
                if "choices" in response_data and len(response_data["choices"]) > 0:
                    answer = response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')

                    # Получаем режим пользователя
                    mode = user_mode_store.get(user_id, 'message')

                    if mode == 'message':
                        # Преобразование Markdown в HTML
                        answer_html = markdown_to_html(answer)

                        # Добавление ответа ассистента в контекст
                        context_store[chat_id][model_id].append({"role": "assistant", "content": answer})

                        # Разбивка длинного ответа на части и отправка
                        messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
                        for msg in messages:
                            await update.message.reply_text(msg, reply_markup=get_main_buttons())
                    elif mode == 'file':
                        # Сохранение ответа в .md файл
                        file_path = save_md_file(answer, user_id)
                        if file_path:
                            # Отправка файла пользователю
                            try:
                                with open(file_path, 'rb') as f:
                                    await update.message.reply_document(
                                        document=InputFile(f),
                                        filename=os.path.basename(file_path),
                                        caption="ваш ответ модели в формате markdown.",
                                        reply_markup=get_main_buttons()
                                    )
                                
                                # Удаление файла после отправки
                                os.remove(file_path)
                            except Exception as e:
                                logger.error(f"Ошибка при отправке файла: {e}")
                                await update.message.reply_text('не удалось отправить файл. попробуйте позже.', reply_markup=get_main_buttons())
                        else:
                            await update.message.reply_text('не удалось создать файл с ответом.', reply_markup=get_main_buttons())
            elif model_info.get('type') == 'image':
                # Обработка ответа для image моделей
                image_data = response_data.get('data')
                if isinstance(image_data, list) and len(image_data) > 0:
                    for image in image_data:
                        image_url = image.get('url')
                        if image_url:
                            # Отправка изображения пользователю
                            await update.message.reply_photo(photo=image_url, reply_markup=get_main_buttons())
                        else:
                            await update.message.reply_text('не удалось получить url изображения.', reply_markup=get_main_buttons())
                else:
                    await update.message.reply_text('не удалось получить изображение. попробуй позже.', reply_markup=get_main_buttons())
    else:
        logger.error(f"Непредвиденный статус ответа API: {response.status_code}")
        await update.message.reply_text('api вернул непредвиденный ответ. попробуй позже.', reply_markup=get_main_buttons())

# Обработчик кнопки "меню"
async def show_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Отправляет пользователю кнопки меню.
    """
    menu_text = "ты в главном меню. выбери действие ниже:"
    
    # Создаем кнопки в меню
    menu_buttons = [
        [InlineKeyboardButton("сменить модель", callback_data='change_model'),
         InlineKeyboardButton("продолжить", callback_data='continue_response')]
    ]
    reply_markup = InlineKeyboardMarkup(menu_buttons)

    await update.message.reply_text(menu_text, parse_mode=ParseMode.HTML, reply_markup=reply_markup)

# Обработчик нажатия кнопки "Сменить модель"
async def change_model_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Обрабатывает нажатие кнопки 'сменить модель'."""
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался сменить модель: {user_id}")
        await query.edit_message_text('тебе сюда мяукать нельзя.')
        return

    # Отображаем кнопки выбора модели
    await query.edit_message_text('выбери модель для взаимодействия:', reply_markup=get_model_buttons(user_id))

# Обработчик нажатия кнопки "Продолжить"
async def continue_response(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Обрабатывает нажатие кнопки 'продолжить'."""
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался продолжить ответ: {user_id}")
        await query.edit_message_text('тебе сюда мяукать нельзя.')
        return

    chat_id = update.effective_chat.id
    selected_model_id = model_store.get(chat_id)
    model_info = model_id_to_info.get(selected_model_id)

    if not model_info:
        logger.error(f"Модель не найдена в конфигурации для чата {chat_id}: {selected_model_id}")
        await query.edit_message_text('модель не найдена в конфигурации.')
        return

    selected_model_type = model_info.get('type', 'text')

    # Проверка доступа к модели
    if selected_model_id in RESTRICTED_MODELS and user_id != AUTHORIZED_MODEL_USER:
        logger.warning(f"Пользователь {user_id} попытался продолжить ответ в ограниченной модели {selected_model_id}")
        await query.edit_message_text('вам нельзя использовать эту модель.')
        return

    # Получаем режим пользователя
    mode = user_mode_store.get(user_id, 'message')

    if selected_model_type in ['text', 'multimodal']:
        # Добавление сообщения пользователя для продолжения
        context_store[chat_id][selected_model_id].append({"role": "user", "content": "продолжи."})

        # Подготовка данных для API с учетом контекста
        data = {
            "model": selected_model_id,
            "messages": context_store[chat_id][selected_model_id]
        }
        api_url = API_URL_TEXT

        try:
            # Отправка запроса в API
            response = requests.post(api_url, headers=API_HEADERS, json=data)
            response.raise_for_status()
        except requests.exceptions.RequestException as e:
            logger.error(f"Ошибка запроса к API: {e}")
            await query.message.reply_text('api не отвечает или произошла ошибка при обработке запроса. попробуй позже.', reply_markup=get_main_buttons())
            return

        if response.status_code == 200:
            response_data = response.json()
            logger.info(f"Ответ API: {response_data}")

            # Проверка структуры ответа для текстовых моделей
            if "choices" in response_data and len(response_data["choices"]) > 0:
                answer = response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
                
                if mode == 'message':
                    # Преобразование Markdown в HTML
                    answer_html = markdown_to_html(answer)
                    
                    # Добавление ответа ассистента в контекст
                    context_store[chat_id][selected_model_id].append({"role": "assistant", "content": answer})
                    
                    # Разбивка длинного ответа на части и отправка
                    messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
                    for msg in messages:
                        await query.message.reply_text(msg, reply_markup=get_main_buttons())
                elif mode == 'file':
                    # Сохранение ответа в .md файл
                    file_path = save_md_file(answer, user_id)
                    if file_path:
                        # Отправка файла пользователю
                        try:
                            with open(file_path, 'rb') as f:
                                await query.message.reply_document(
                                    document=InputFile(f),
                                    filename=os.path.basename(file_path),
                                    caption="твой ответ модели в формате markdown.",
                                    reply_markup=get_main_buttons()
                                )
                            
                            # Удаление файла после отправки
                            os.remove(file_path)
                        except Exception as e:
                            logger.error(f"Ошибка при отправке файла: {e}")
                            await query.message.reply_text('не удалось отправить файл. попробуйте позже.', reply_markup=get_main_buttons())
                    else:
                        await query.message.reply_text('не удалось создать файл с ответом.', reply_markup=get_main_buttons())
    elif selected_model_type == 'image':
        await query.message.reply_text('функция "продолжить" не поддерживается для выбранной модели.', reply_markup=get_main_buttons())
    else:
        await query.message.reply_text('неизвестный тип модели.', reply_markup=get_main_buttons())

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

# Основная функция запуска бота
def main():
    # Загрузка конфигурации уже произведена выше
    TELEGRAM_TOKEN = config.get('telegram_token')
    if not TELEGRAM_TOKEN:
        logger.error("Токен бота не указан в config.json.")
        exit(1)
    
    # Создание экземпляра приложения с установкой ParseMode.HTML по умолчанию
    application = ApplicationBuilder().token(TELEGRAM_TOKEN).defaults(Defaults(parse_mode=ParseMode.HTML)).build()

    # Обработчики команд
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("set_mode", set_mode))
    application.add_handler(CommandHandler("get_mode", get_mode))
    application.add_handler(CommandHandler("instructions", instructions))
    application.add_handler(CommandHandler("models", models_command))  # Добавлена команда /models
    
    # Специфичные обработчики CallbackQuery сначала
    # Сначала обработчики с более специфичными паттернами
    # Создаём регулярное выражение для выбора моделей на основе model_id
    model_ids = list(model_id_to_info.keys())
    if model_ids:
        # Экранируем специальные символы в model_id для регулярного выражения
        escaped_model_ids = [re.escape(model_id) for model_id in model_ids]
        pattern_models = f'^({"|".join(escaped_model_ids)})$'
        pattern_restricted = f'^restricted:({"|".join(escaped_model_ids)})$'
        # Обработчик выборки моделей
        application.add_handler(CallbackQueryHandler(select_model, pattern=pattern_models))
        # Обработчик ограниченных моделей
        application.add_handler(CallbackQueryHandler(select_model, pattern=pattern_restricted))
    
    # Обработчики CallbackQuery для основных кнопок
    application.add_handler(CallbackQueryHandler(change_model_callback, pattern='^change_model$'))
    application.add_handler(CallbackQueryHandler(continue_response, pattern='^continue_response$'))
    
    # Обработчики сообщений
    # Обработка текстовых сообщений
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
    
    # Обработка документов (файлов)
    application.add_handler(MessageHandler(filters.Document.ALL, handle_document))
    
    # Обработчик ошибок
    application.add_error_handler(error_handler)

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

if __name__ == '__main__':
    main()
код config.json: 
{
    "telegram_token": "токен",
    "api_key": "ключ",
    "authorized_user_ids": [
        5005816138,
        7856892361,
        5643392602,
        7193906122,
        5075150336
    ],
    "models": {
        "⚡️Claude 3.5 Sonnet": {
            "id": "claude-3-5-sonnet-20241022",
            "type": "multimodal"
        },
        "Claude 3 Opus": {
            "id": "claude-3-opus",
            "type": "multimodal"
        },
        "⚡️GPT-4o-latest": {
            "id": "chatgpt-4o-latest",
            "type": "multimodal"
        },
        "GPT-4 Turbo": {
            "id": "gpt-4-turbo",
            "type": "multimodal"
        },
        "GPT-4o mini": {
            "id": "gpt-4o-mini",
            "type": "multimodal"
        },
        "💥o1-preview": {
            "id": "o1-preview",
            "type": "text"
        },
        "💥o1-mini": {
            "id": "o1-mini",
            "type": "text"
        },
        "LLaMA 3.1 405b": {
            "id": "llama-3.1-405b-turbo",
            "type": "text"
        },
        "✨Flux 1.1 Pro": {
            "id": "FLUX.1.1 [pro]",
            "type": "image"
        },
        "Stable Diffusion 3.5": {
            "id": "sd3.5",
            "type": "image"
        }
    },
    "system_instruction": "ты - хороший ассистент, готовый ответить на любой вопрос."
}
отправь мне полные коды изменённых файлов с указанием внесённых изменений.