Два года назад я написал статью о том, как сделать ChatGPT-бота в Telegram через ProxyAPI. С тех пор многое изменилось: появились новые модели, новые возможности, а сам ProxyAPI обзавёлся универсальным OpenAI-совместимым API.
Пришло время обновить бота. В этой статье я покажу, как создать Telegram-бота, который умеет:
Общаться с любой AI-моделью — OpenAI, Anthropic, Google, DeepSeek и сотни других
Переключаться между моделями на лету
Анализировать изображения (vision)
Генерировать изображения по текстовому описанию
Искать в интернете актуальную информацию
Запоминать контекст диалога
И всё это — через единый API-endpoint, без VPN, с оплатой в рублях.
ProxyAPI — это сервис, который даёт доступ к API мировых лидеров в области AI (OpenAI, Anthropic, Google, DeepSeek) из России без VPN и блокировок. Оплата в рублях, не нужен иностранный телефон или карта.
Ключевая фишка, которую я использую в этой статье — OpenAI-совместимый API. Это универсальный шлюз, который принимает запросы в формате OpenAI и автоматически маршрутизирует их к нужному провайдеру. На входе вы работаете с единым протоколом, а конвертация форматов выполняется на стороне ProxyAPI.
Базовый адрес:
https://openai.api.proxyapi.ru/v1
Модели адресуются в формате провайдер/модель:
openai/gpt-5.2
anthropic/claude-sonnet-4-5
gemini/gemini-2.5-flash
openrouter/deepseek/deepseek-v3.2
Это значит, что один и тот же код работает с любой моделью — достаточно изменить строку с именем модели.
Для реализации проекта понадобится:
Регистрируемся на ProxyAPI, идём в раздел Ключи API и создаём ключ.
Сохраните ключ при создании! В полном виде вы его больше не увидите.
Создаём бота через @BotFather в Telegram:
Отправляем /newbot
Даём имя и username
Сохраняем токен
Если аккаунта ещё нет — создайте его на cloud.yandex.ru. Убедитесь, что подключён платёжный аккаунт в статусе ACTIVE или TRIAL_ACTIVE.
Все ресурсы, которые будут использоваться, имеют ежемесячный бесплатный лимит. При личном использовании бота вся инфраструктура обойдётся бесплатно.
В консоли Яндекс Облака переходим в Сервисные аккаунты и создаём новый. Присваиваем роли:
serverless.functions.invoker
storage.uploader
storage.viewer
После создания переходим в аккаунт и создаём статический ключ доступа. Сохраняем Key ID и Secret.
Переходим в Object Storage и создаём новый бакет. Настройки по умолчанию.
В этом бакете будет храниться история чатов и настройки пользователей.
Бот состоит из трёх файлов:
requirements.txt
openai
aiogram
aiohttp
boto3
openai — официальный SDK OpenAI, работает с ProxyAPI благодаря совместимому API
aiogram — современный асинхронный фреймворк для Telegram-ботов
boto3 — AWS SDK для работы с Yandex Object Storage (S3-совместимое хранилище)
aiohttp — для скачивания сгенерированных изображений
bot.py — основная логика
Это главный файл бота. Разберём его по частям.
Инициализация
import os
import json
import asyncio
import logging
import base64
import re
import aiohttp
import boto3
from aiogram import Bot, Dispatcher, types, F
from aiogram.filters import Command, CommandObject
from aiogram.enums import ParseMode, ChatAction
from aiogram.types import BufferedInputFile
from openai import AsyncOpenAI
logging.basicConfig(level=logging.INFO)
TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN")
PROXYAPI_KEY = os.getenv("PROXYAPI_KEY")
YANDEX_KEY_ID = os.getenv("YANDEX_KEY_ID")
YANDEX_KEY_SECRET = os.getenv("YANDEX_KEY_SECRET")
YANDEX_BUCKET = os.getenv("YANDEX_BUCKET")
PROXYAPI_BASE_URL = "https://openai.api.proxyapi.ru/v1"
DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
IMAGE_MODEL = "openai/gpt-image-1.5"
MAX_HISTORY_CHARS = 50_000
Здесь используется AsyncOpenAI — асинхронный клиент OpenAI SDK. Благодаря OpenAI-совместимому API ProxyAPI достаточно указать base_url, и SDK будет работать с любой моделью через ProxyAPI.
bot = Bot(token=TG_BOT_TOKEN)
dp = Dispatcher()
client = AsyncOpenAI(
api_key=PROXYAPI_KEY,
base_url=PROXYAPI_BASE_URL,
)
Список популярных моделей для быстрого переключения через inline-клавиатуру:
MODEL_SHORTCUTS = [
("Claude Sonnet 4.5", "anthropic/claude-sonnet-4-5"),
("GPT-5.2", "openai/gpt-5.2"),
("Gemini 2.5 Flash", "gemini/gemini-2.5-flash"),
("DeepSeek V3.2", "openrouter/deepseek/deepseek-v3.2"),
]
Это лишь шорткаты — пользователь может указать любую модель вручную.
Хранилище данных (S3)
Для хранения истории чатов и настроек используем Yandex Object Storage через AWS SDK:
def get_s3_client():
session = boto3.session.Session(
aws_access_key_id=YANDEX_KEY_ID,
aws_secret_access_key=YANDEX_KEY_SECRET,
)
return session.client(
service_name="s3",
endpoint_url="https://storage.yandexcloud.net",
)
def load_user_data(user_id: int) -> dict:
try:
s3 = get_s3_client()
obj = s3.get_object(Bucket=YANDEX_BUCKET, Key=f"{user_id}.json")
return json.loads(obj["Body"].read())
except Exception:
return {"model": DEFAULT_MODEL, "history": []}
def save_user_data(user_id: int, data: dict):
s3 = get_s3_client()
s3.put_object(
Bucket=YANDEX_BUCKET,
Key=f"{user_id}.json",
Body=json.dumps(data, ensure_ascii=False),
)
Для каждого пользователя создаётся JSON-файл в бакете с его историей и выбранной моделью:
{
"model": "anthropic/claude-sonnet-4-5",
"history": [
{"role": "user", "content": "Привет!"},
{"role": "assistant", "content": "Здравствуйте!"}
]
}
Вспомогательные функции
Индикатор «печатает...» — Telegram показывает его всего ~5 секунд, поэтому отправляем повторно каждые 4 секунды через фоновую async-задачу:
async def keep_typing(chat_id: int):
while True:
await bot.send_chat_action(chat_id, ChatAction.TYPING)
await asyncio.sleep(4)
Разбивка длинных ответов — Telegram ограничивает сообщения 4096 символами:
MAX_MESSAGE_LENGTH = 4096
async def send_long_message(message: types.Message, text: str):
for i in range(0, len(text), MAX_MESSAGE_LENGTH):
chunk = text[i:i + MAX_MESSAGE_LENGTH]
try:
await message.answer(chunk, parse_mode=ParseMode.MARKDOWN)
except Exception:
await message.answer(chunk)
Ответ отправляется с Markdown-форматированием. Если Telegram не может его распарсить (бывает, что модели генерируют невалидный Markdown), отправляем как plain text.
Команды бота
@dp.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer(
f"Привет! {HELP_TEXT}",
parse_mode=ParseMode.HTML,
)
@dp.message(Command("new"))
async def cmd_new(message: types.Message):
user_data = load_user_data(message.from_user.id)
user_data["history"] = []
save_user_data(message.from_user.id, user_data)
await message.answer("История чата очищена.")
@dp.message(Command("model"))
async def cmd_model(message: types.Message):
user_data = load_user_data(message.from_user.id)
model = user_data.get("model", DEFAULT_MODEL)
await message.answer(f"Текущая модель: <code>{model}</code>", parse_mode=ParseMode.HTML)
Переключение моделей
Команда /setmodel работает двумя способами:
/setmodel — показывает inline-клавиатуру с популярными моделями
/setmodel anthropic/claude-sonnet-4-5 — устанавливает произвольную модель напрямую
@dp.message(Command("setmodel"))
async def cmd_setmodel(message: types.Message, command: CommandObject):
if command.args:
user_data = load_user_data(message.from_user.id)
user_data["model"] = command.args.strip()
save_user_data(message.from_user.id, user_data)
await message.answer(
f"Модель изменена на: <code>{command.args.strip()}</code>",
parse_mode=ParseMode.HTML,
)
return
keyboard = []
for name, model_id in MODEL_SHORTCUTS:
keyboard.append([types.InlineKeyboardButton(
text=name, callback_data=f"setmodel:{model_id}"
)])
markup = types.InlineKeyboardMarkup(inline_keyboard=keyboard)
await message.answer(
"Выберите модель или отправьте /setmodel <id>:",
reply_markup=markup, parse_mode=ParseMode.HTML,
)
@dp.callback_query(F.data.startswith("setmodel:"))
async def callback_setmodel(callback: types.CallbackQuery):
model_id = callback.data.split(":", 1)[1]
user_data = load_user_data(callback.from_user.id)
user_data["model"] = model_id
save_user_data(callback.from_user.id, user_data)
await callback.message.edit_text(
f"Модель изменена на: <code>{model_id}</code>",
parse_mode=ParseMode.HTML,
)
await callback.answer()
Никаких ограничений на выбор модели нет — если ProxyAPI поддерживает модель, она будет работать. Inline-клавиатура — лишь шорткаты для удобства.
Генерация изображений
@dp.message(Command("image"))
async def cmd_image(message: types.Message, command: CommandObject):
prompt = command.args
if not prompt:
await message.answer("Укажите промпт: /image <описание>",
parse_mode=ParseMode.HTML)
return
typing_task = asyncio.create_task(keep_typing(message.chat.id))
try:
response = await client.images.generate(
model=IMAGE_MODEL,
prompt=prompt,
n=1,
size="1024x1024",
)
image_url = response.data[0].url
if image_url:
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as resp:
image_bytes = await resp.read()
photo = BufferedInputFile(image_bytes, filename="generated.png")
await message.answer_photo(photo)
elif response.data[0].b64_json:
image_bytes = base64.b64decode(response.data[0].b64_json)
photo = BufferedInputFile(image_bytes, filename="generated.png")
await message.answer_photo(photo)
else:
await message.answer("Не удалось получить изображение.")
except Exception as e:
await message.answer(f"Ошибка генерации: {e}")
finally:
typing_task.cancel()
Генерация работает через endpoint /v1/images/generations OpenAI-совместимого API. API может вернуть изображение либо как URL, либо как base64 — обрабатываем оба варианта.
Главная функция — отправка сообщения в AI
Это ядро бота. Здесь используется Responses API вместо привычного Chat Completions — потому что именно Responses API поддерживает встроенные инструменты, такие как веб-поиск:
async def send_to_ai(user_id: int, content, message: types.Message):
user_data = load_user_data(user_id)
model = user_data.get("model", DEFAULT_MODEL)
history = user_data.get("history", [])
if history_chars(history) > MAX_HISTORY_CHARS:
await message.answer(
"История чата слишком длинная. "
"Отправьте /new чтобы очистить и начать заново."
)
return
history.append({"role": "user", "content": content})
typing_task = asyncio.create_task(keep_typing(message.chat.id))
try:
response = await client.responses.create(
model=model,
input=history,
tools=[{"type": "web_search"}],
)
ai_text = response.output_text
history.append({"role": "assistant", "content": ai_text})
except Exception as e:
ai_text = f"Ошибка: {e}"
history.pop()
finally:
typing_task.cancel()
user_data["history"] = history
save_user_data(user_id, user_data)
await send_long_message(message, ai_text)
Обратите внимание на tools=[{"type": "web_search"}]. Это даёт модели возможность искать актуальную информацию в интернете. Модель сама решает, когда нужен поиск — для обычных вопросов она отвечает из своих знаний, а для вопросов о текущих событиях, погоде и т.д. автоматически выполнит поиск.
Через OpenAI-совместимый API ProxyAPI веб-поиск работает с моделями OpenAI, Anthropic и Google. Для моделей через OpenRouter поиск будет проигнорирован без ошибки.
Обработка фотографий (Vision)
@dp.message(F.photo)
async def handle_photo(message: types.Message):
photo = message.photo[-1]
file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path)
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
caption = message.caption or "Что изображено на этой картинке?"
content = [
{"type": "input_text", "text": caption},
{"type": "input_image", "image_url": f"dаta:image/jpeg;base64,{image_b64}"},
]
await send_to_ai(message.from_user.id, content, message)
Когда пользователь отправляет фото, бот скачивает его, кодирует в base64 и отправляет модели. Если к фото есть подпись — она используется как промпт. Если нет — бот попросит модель описать, что на картинке.
Важно: изображения сохраняются в истории чата в формате base64. Это значит, что при каждом последующем запросе они отправляются повторно, что увеличивает стоимость. Если для вас это критично, можно после получения ответа заменять vision-сообщение на текстовое с подписью.
Обработка текстовых сообщений
@dp.message(F.text & ~F.text.startswith("/"))
async def handle_text(message: types.Message):
await send_to_ai(message.from_user.id, message.text, message)
@dp.message(Command(re.compile(r".*")))
async def handle_unknown_command(message: types.Message):
await message.answer("Неизвестная команда. Отправьте /help для списка команд.")
index.py — точка входа для Cloud Function
import json
import asyncio
from aiogram.types import Update
from bot import bot, dp
loop = asyncio.new_event_loop()
async def process(event):
body = json.loads(event["body"])
update = Update(**body)
await dp.feed_update(bot, update)
def handler(event, context):
loop.run_until_complete(process(event))
return {"statusCode": 200, "body": "ok"}
Этот файл — адаптер между Yandex Cloud Functions и aiogram. Облачная функция получает HTTP-запрос от Telegram через API Gateway, парсит его в объект Update и передаёт диспетчеру aiogram для обработки.
Нюанс с event loop: здесь asyncio.new_event_loop() создаётся на уровне модуля, а не через asyncio.run(). Это важно, потому что Yandex Cloud Functions переиспользует процесс между вызовами. asyncio.run() закрывает event loop после завершения, и при следующем вызове aiohttp-сессия внутри aiogram окажется привязана к закрытому loop. Создание loop на уровне модуля решает эту проблему.
Создаём новую функцию в разделе Cloud Functions:
Выбираем среду выполнения Python 3.14:
Здесь доступен весь код этого проекта. Можно клонировать его локально, собрать ZIP-архив и загрузить его в облачную функцию. Либо вручную создать одноименные файлы в редакторе облачной функции c таким же содержимым, как в репозитории:
bot.py
index.py
requirements.txt
Приведу пример с клонированием и ZIP-архивом:
git clone https://gitlab.com/evrovas/chatgpt-telegram-bot-proxyapi-2026.git
cd chatgpt-telegram-bot-proxyapi-2026
zip -r function.zip bot.py index.py requirements.txt
Настраиваем параметры:
Точка входа: index.handler
Таймаут: 3 минуты (генерация изображений и веб-поиск могут быть долгими)
Память: 256 МБ
Сервисный аккаунт: выбираем созданный ранее
Переменные окружения: TG_BOT_TOKEN, PROXYAPI_KEY, YANDEX_KEY_ID, YANDEX_KEY_SECRET, YANDEX_BUCKET
После создание функции надо перейти в раздел Обзор и скопировать оттуда идентификатор функции. Он понадобится нам в следующем шаге.
Создаём API Gateway со следующей спецификацией:
openapi: 3.0.0
info:
title: Telegram Bot API
version: 1.0.0
paths:
/:
post:
x-yc-apigateway-integration:
type: cloud-functions
function_id: <ID_ФУНКЦИИ>
service_account_id: <ID_СЕРВИСНОГО_АККАУНТА>
После сохранения на странице появится Служебный домен - копируем его для следующего шага.
Последний шаг — сообщить Telegram, куда пересылать сообщения:
curl -X POST "https://api.telegram.org/bot<ТОКЕН>/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url": "<ДОМЕН_API_ШЛЮЗА>"}'
Должны получить ответ:
{"ok": true, "result": true, "description": "Webhook was set"}
Всё готово! Проверяем работу бота:
Бот запоминает контекст, позволяет переключать модели и автоматически ищет актуальную информацию в интернете, когда это нужно.
Команда /image кот в сапогах сгенерировала изображение через модель openai/gpt-image-1.5.
Отправляем фото — бот описывает, что на нём изображено.
Бесплатные лимиты за каждый месяц:
Cloud Functions: 1 млн вызовов, 10 ГБ/ч использования памяти
Object Storage: 1 ГБ хранения, 10 000 PUT/POST, 100 000 GET
API Gateway: 100 000 запросов
При личном использовании бота в эти ограничения можно укладываться с большим запасом.
Стоимость зависит от выбранной модели и объёма использования. Актуальные цены — на странице тарифов ProxyAPI.
Получился полноценный AI-бот в Telegram, который за два года эволюции ушёл далеко от простого ChatGPT-прокси:
Переключение между моделями OpenAI, Anthropic, Google и сотнями моделей через OpenRouter — одной командой
Анализ изображений, генерация картинок, автоматический веб-поиск
Всё через один API-endpoint — не нужно разбираться в форматах каждого провайдера отдельно
Бесплатная serverless-инфраструктура на Yandex Cloud
Для меня ProxyAPI оказался самым удобным способом быстро подключить сразу все модели AI без возни с VPN, иностранными картами и отдельными аккаунтами у каждого провайдера.
Весь код — на GitLab.