Just wanted to share that by using Vercel AI Gateway you don’t need to make your own proxy to bypass the regional blocking.
I was about atleast get it to respond: https://a-proxy-request-three.vercel.app/api/health
I moved the index.py to the root folder and updated the code to for more debug logs:
import os
import logging
import traceback
from pathlib import Path
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import create_engine, Column, Integer, String, Boolean, BigInteger, Text, DateTime, LargeBinary, ForeignKey
from sqlalchemy.orm import sessionmaker, declarative_base, Session
from sqlalchemy.exc import SQLAlchemyError, OperationalError, DatabaseError
from groq import Groq
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# --- 1. ОБЩАЯ НАСТРОЙКА ---
app = FastAPI()
# --- 2. НАСТРОЙКА ДЛЯ ПРОКСИ-GROQ ---
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
# --- 3. НАСТРОЙКА ДЛЯ ВЕБ-ИСТОРИИ (SERVERLESS-СОВМЕСТИМАЯ) ---
DATABASE_URL = os.getenv("DATABASE_URL")
Base = declarative_base()
# Для Vercel serverless - без connection pooling
def get_engine():
"""Create database engine with error handling"""
if not DATABASE_URL:
logger.warning("DATABASE_URL is not configured")
return None
try:
logger.info("Creating database engine...")
engine = create_engine(
DATABASE_URL,
poolclass=None, # Отключаем pooling для serverless
connect_args={"connect_timeout": 10}
)
logger.info("Database engine created successfully")
return engine
except OperationalError as e:
logger.error(f"Database operational error: {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
return None
except DatabaseError as e:
logger.error(f"Database error: {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
return None
except Exception as e:
logger.error(f"Unexpected error creating database engine: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
return None
# Template path - работает как локально, так и на Vercel
try:
template_dir = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(template_dir))
logger.info(f"Templates directory set to: {template_dir}")
except Exception as e:
logger.error(f"Error setting up templates: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
# Fallback to relative path
templates = Jinja2Templates(directory="templates")
# --- МОДЕЛИ ДЛЯ ВЕБ-ИСТОРИИ (SQLAlchemy) ---
class Pair(Base):
__tablename__ = "pairs"
id = Column(Integer, primary_key=True)
client_id = Column(BigInteger, unique=True, nullable=False)
performer_id = Column(BigInteger, unique=True, nullable=False)
is_frozen = Column(Boolean, default=False)
class User(Base):
__tablename__ = "users"
user_id = Column(BigInteger, primary_key=True, autoincrement=False)
display_name = Column(String, nullable=False)
class History(Base):
__tablename__ = "history"
id = Column(Integer, primary_key=True)
pair_id = Column(Integer, ForeignKey("pairs.id"), nullable=False)
message_id = Column(BigInteger, nullable=False, index=True)
sender_id = Column(BigInteger, nullable=False)
date = Column(DateTime, nullable=False)
text = Column(Text)
original_text = Column(Text)
media_blob = Column(LargeBinary)
media_url = Column(String) # Добавлено недостающее поле
status = Column(String, default="sent")
edit_date = Column(DateTime)
log_message_id = Column(Integer)
# --- ЛОГИКА ДЛЯ ПРОКСИ-GROQ ---
MODEL_INFO = {
"llama3-70b-8192": {"size": 70, "quality": 10}, "llama3-8b-8192": {"size": 8, "quality": 7},
"llama-3.1-70b-versatile": {"size": 70, "quality": 9.5}, "mixtral-8x7b-32768": {"size": 56, "quality": 8.5},
"gemma-7b-it": {"size": 7, "quality": 6},
}
def find_best_alternative(models_list):
best_model, max_score = None, -1
for model_id in models_list:
score = MODEL_INFO.get(model_id, {}).get("quality", 0)
if score > max_score:
max_score, best_model = score, model_id
return best_model or "gemma-7b-it"
@app.post("/api/groq")
async def proxy_to_groq(request: Request):
"""Proxy endpoint for Groq API with comprehensive error handling"""
if not groq_client:
logger.error("Groq API call attempted but GROQ_API_KEY is not set")
raise HTTPException(status_code=500, detail="GROQ_API_KEY is not set.")
try:
logger.info("Received Groq API proxy request")
data = await request.json()
model = data.get("model", "llama-3.1-70b-versatile")
if "messages" not in data:
logger.error("Missing 'messages' field in request data")
raise HTTPException(status_code=400, detail="Missing 'messages' field in request")
logger.info(f"Attempting to use model: {model}")
try:
chat_completion = groq_client.chat.completions.create(messages=data["messages"], model=model)
logger.info(f"Successfully completed chat with model: {model}")
except Exception as e:
error_str = str(e)
logger.warning(f"Error with model {model}: {error_str}")
if "model_decommissioned" in error_str or "not found" in error_str.lower():
logger.info("Model decommissioned, attempting to find alternative...")
try:
models_response = groq_client.models.list()
available_ids = [m.id for m in models_response.data]
best_alternative = find_best_alternative(available_ids)
logger.info(f"Switching from {model} to {best_alternative}")
chat_completion = groq_client.chat.completions.create(messages=data["messages"], model=best_alternative)
logger.info(f"Successfully completed chat with alternative model: {best_alternative}")
except Exception as fallback_error:
logger.error(f"Failed to use alternative model: {type(fallback_error).__name__} - {str(fallback_error)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(
status_code=500,
detail=f"Failed with original and alternative models: {str(fallback_error)}"
)
elif "rate_limit" in error_str.lower():
logger.error(f"Rate limit error: {error_str}")
raise HTTPException(status_code=429, detail=f"Rate limit exceeded: {error_str}")
elif "authentication" in error_str.lower() or "api key" in error_str.lower():
logger.error(f"Authentication error: {error_str}")
raise HTTPException(status_code=401, detail=f"Authentication failed: {error_str}")
else:
logger.error(f"Unexpected Groq API error: {type(e).__name__} - {error_str}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise e
return chat_completion.dict()
except HTTPException:
raise # Re-raise HTTP exceptions as-is
except KeyError as e:
logger.error(f"Missing required field: {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=400, detail=f"Missing required field: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in Groq proxy: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error: {type(e).__name__} - {str(e)}")
# --- ЛОГИКА ДЛЯ ВЕБ-ИСТОРИИ (SERVERLESS-СОВМЕСТИМАЯ) ---
def get_db() -> Session:
"""Dependency для получения DB сессии (serverless-совместимая версия) с error handling"""
if not DATABASE_URL:
logger.error("Database access attempted but DATABASE_URL is not set")
raise HTTPException(status_code=500, detail="DATABASE_URL is not set.")
engine = None
db = None
try:
logger.info("Creating database session...")
engine = get_engine()
if not engine:
logger.error("Failed to create database engine - get_engine returned None")
raise HTTPException(status_code=500, detail="Failed to create database engine.")
# Создаем таблицы если их нет (для serverless это безопасно делать на каждом запросе)
try:
Base.metadata.create_all(bind=engine)
logger.info("Database tables checked/created successfully")
except OperationalError as e:
logger.error(f"Database operational error during table creation: {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=503, detail=f"Database connection failed: {str(e)}")
except DatabaseError as e:
logger.error(f"Database error during table creation: {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
except Exception as e:
logger.warning(f"Warning: Could not create tables: {type(e).__name__} - {str(e)}")
# Создаем новую сессию для каждого запроса
try:
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db = SessionLocal()
logger.info("Database session created successfully")
except Exception as e:
logger.error(f"Failed to create database session: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Failed to create database session: {str(e)}")
yield db
except HTTPException:
raise # Re-raise HTTP exceptions as-is
except Exception as e:
logger.error(f"Unexpected error in get_db: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
finally:
# Cleanup resources
if db:
try:
db.close()
logger.debug("Database session closed")
except Exception as e:
logger.warning(f"Error closing database session: {str(e)}")
if engine:
try:
engine.dispose()
logger.debug("Database engine disposed")
except Exception as e:
logger.warning(f"Error disposing database engine: {str(e)}")
def get_user_display_name(db_session, user_id: int) -> str:
"""Get user display name with error handling"""
try:
user = db_session.query(User).filter(User.user_id == user_id).first()
return user.display_name if user else f"ID: {user_id}"
except SQLAlchemyError as e:
logger.error(f"Database error getting user {user_id}: {str(e)}")
return f"ID: {user_id}"
except Exception as e:
logger.error(f"Unexpected error getting user {user_id}: {type(e).__name__} - {str(e)}")
return f"ID: {user_id}"
@app.get("/", response_class=HTMLResponse)
async def list_pairs(request: Request, db: Session = Depends(get_db)):
"""List all conversation pairs with comprehensive error handling"""
try:
logger.info("Fetching list of conversation pairs")
pairs = db.query(Pair).all()
logger.info(f"Found {len(pairs)} conversation pairs")
pair_details = []
for p in pairs:
try:
pair_details.append({
"client_id": p.client_id,
"client_name": get_user_display_name(db, p.client_id),
"performer_name": get_user_display_name(db, p.performer_id),
"is_frozen": p.is_frozen
})
except Exception as e:
logger.error(f"Error processing pair {p.id}: {type(e).__name__} - {str(e)}")
# Continue with other pairs even if one fails
continue
return templates.TemplateResponse("index.html", {"request": request, "pairs": pair_details})
except SQLAlchemyError as e:
logger.error(f"Database error in list_pairs: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=503, detail=f"Database error: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in list_pairs: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error loading pairs: {str(e)}")
@app.get("/dialog/{client_id}", response_class=HTMLResponse)
async def show_dialog(request: Request, client_id: int, db: Session = Depends(get_db)):
"""Show dialog for a specific client with comprehensive error handling"""
try:
logger.info(f"Fetching dialog for client_id: {client_id}")
pair = db.query(Pair).filter(Pair.client_id == client_id).first()
if not pair:
logger.warning(f"Dialog not found for client_id: {client_id}")
raise HTTPException(status_code=404, detail="Диалог не найден")
logger.info(f"Found pair {pair.id} for client {client_id}")
try:
messages = db.query(History).filter(History.pair_id == pair.id).order_by(History.date).all()
logger.info(f"Found {len(messages)} messages for pair {pair.id}")
except SQLAlchemyError as e:
logger.error(f"Database error fetching messages: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=503, detail=f"Error fetching messages: {str(e)}")
client_name = get_user_display_name(db, pair.client_id)
performer_name = get_user_display_name(db, pair.performer_id)
return templates.TemplateResponse("dialog.html", {
"request": request,
"client_id": client_id,
"client_name": client_name,
"performer_name": performer_name,
"messages": messages
})
except HTTPException:
raise # Re-raise HTTP exceptions as-is
except SQLAlchemyError as e:
logger.error(f"Database error in show_dialog: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=503, detail=f"Database error: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in show_dialog: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error loading dialog: {str(e)}")
@app.get("/api/dialog/{client_id}")
async def get_dialog_data(client_id: int, db: Session = Depends(get_db)):
"""Возвращает данные диалога в формате JSON с comprehensive error handling"""
try:
logger.info(f"API: Fetching dialog data for client_id: {client_id}")
pair = db.query(Pair).filter(Pair.client_id == client_id).first()
if not pair:
logger.warning(f"API: Dialog not found for client_id: {client_id}")
raise HTTPException(status_code=404, detail="Диалог не найден")
logger.info(f"API: Found pair {pair.id} for client {client_id}")
try:
messages = db.query(History).filter(History.pair_id == pair.id).order_by(History.date).all()
logger.info(f"API: Found {len(messages)} messages for pair {pair.id}")
except SQLAlchemyError as e:
logger.error(f"Database error fetching messages in API: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=503, detail=f"Error fetching messages: {str(e)}")
# Преобразуем сообщения в формат, удобный для JSON
messages_data = []
for msg in messages:
try:
messages_data.append({
"id": msg.id,
"sender_id": msg.sender_id,
"text": msg.text,
"original_text": msg.original_text,
"date": msg.date.isoformat() if msg.date else None,
"status": msg.status,
"media_url": msg.media_url
})
except Exception as e:
logger.error(f"Error serializing message {msg.id}: {type(e).__name__} - {str(e)}")
# Continue with other messages even if one fails
continue
return {"messages": messages_data}
except HTTPException:
raise # Re-raise HTTP exceptions as-is
except SQLAlchemyError as e:
logger.error(f"Database error in get_dialog_data: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=503, detail=f"Database error: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in get_dialog_data: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error fetching dialog data: {str(e)}")
# --- HEALTH CHECK ---
@app.get("/api/health")
def health_check():
"""Health check endpoint с comprehensive diagnostics"""
try:
logger.info("Health check requested")
# Test database connection if configured
db_status = "not_configured"
db_error = None
if DATABASE_URL:
try:
engine = get_engine()
if engine:
# Try to execute a simple query
with engine.connect() as conn:
conn.execute("SELECT 1")
db_status = "connected"
logger.info("Health check: Database connection successful")
else:
db_status = "error"
db_error = "Failed to create engine"
logger.warning("Health check: Failed to create database engine")
except Exception as e:
db_status = "error"
db_error = str(e)
logger.error(f"Health check: Database connection failed: {str(e)}")
# Test Groq client if configured
groq_status = "not_configured"
groq_error = None
if GROQ_API_KEY:
if groq_client:
groq_status = "configured"
logger.info("Health check: Groq client configured")
else:
groq_status = "error"
groq_error = "Failed to initialize client"
logger.warning("Health check: Groq client initialization failed")
status = {
"status": "ok" if db_status in ["connected", "not_configured"] and groq_status in ["configured", "not_configured"] else "degraded",
"groq": {
"status": groq_status,
"error": groq_error
},
"database": {
"status": db_status,
"error": db_error
},
"timestamp": os.popen('date -u +"%Y-%m-%dT%H:%M:%SZ"').read().strip()
}
logger.info(f"Health check result: {status['status']}")
return status
except Exception as e:
logger.error(f"Error in health check: {type(e).__name__} - {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
return {
"status": "error",
"error": str(e),
"type": type(e).__name__
}
# --- GLOBAL EXCEPTION HANDLER ---
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Global exception handler to catch any unhandled errors"""
logger.error(f"UNHANDLED EXCEPTION: {type(exc).__name__} - {str(exc)}")
logger.error(f"Request URL: {request.url}")
logger.error(f"Request method: {request.method}")
logger.error(f"Traceback: {traceback.format_exc()}")
# Return a generic error response
return JSONResponse(
status_code=500,
content={
"error": "Internal server error",
"type": type(exc).__name__,
"detail": str(exc),
"path": str(request.url)
}
)
# Log startup
logger.info("=" * 60)
logger.info("Application starting up...")
logger.info(f"GROQ_API_KEY configured: {GROQ_API_KEY is not None}")
logger.info(f"DATABASE_URL configured: {DATABASE_URL is not None}")
logger.info("=" * 60)
You should be able to get some information from the logs.