ช่วงเดือนที่ผ่านมา ทีมของผมเจอปัญหาหนักใจจากระบบ AI gateway ที่ใช้อยู่ วันหนึ่งเซิร์ฟเวอร์ส่ง error มาหลายจุดพร้อมกัน:
ConnectionError: HTTPSConnectionPool(host='api.openai.com', port=443):
Max retries exceeded with url: /v1/chat/completions
RateLimitError: 429 Too Many Requests - Rate limit reached for gpt-4.1
AuthenticationError: 401 Unauthorized - Invalid API key
GeminiError: Quota exceeded for model gemini-2.5-flash
ปัญหาคือเราใช้หลายโมเดลพร้อมกันใน production แต่ไม่มีระบบจัดการ quota ที่ดี เลยเกิดการชนกันของ request ทำให้บาง feature ล่มไป 3 ชั่วโมง หลังจากนั้นผมเลยตัดสินใจสร้าง multi-model router ที่จัดการเรื่องนี้อย่างเป็นระบบ และนำมาแบ่งปันในบทความนี้ครับ
ทำไมต้องมี Multi-Model Router?
ในปี 2026 การใช้ AI model เพียงตัวเดียวไม่เพียงพออีกต่อไป เพราะแต่ละโมเดลมีจุดเด่นต่างกัน:
- Claude Sonnet 4.5 - เหมาะกับงานเขียนโค้ดและ reasoning ซับซ้อน
- GPT-4.1 - ดีในงาน creative และ general purpose
- Gemini 2.5 Flash - เร็วมาก ราคาถูก เหมาะกับงานที่ต้องการ latency ต่ำ
- DeepSeek V3.2 - ราคาถูกที่สุดในกลุ่ม เหมาะกับงาน bulk processing
ปัญหาคือแต่ละ provider มี rate limit และ quota ที่ต่างกัน ถ้าเราไม่จัดการให้ดี ระบบจะล่มเมื่อ request มากเกินไป หรือเสียเงินค่า overage แพงกว่าเดิม
สถาปัตยกรรม Multi-Model Router
import httpx
import asyncio
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable
from enum import Enum
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class Provider(Enum):
ANTHROPIC = "anthropic"
OPENAI = "openai"
GEMINI = "gemini"
DEEPSEEK = "deepseek"
@dataclass
class RateLimitConfig:
requests_per_minute: int = 60
tokens_per_minute: int = 100000
concurrent_requests: int = 10
@dataclass
class QuotaStatus:
used_today: float = 0.0
limit_today: float = 0.0
remaining: float = 0.0
reset_time: Optional[float] = None
@dataclass
class ProviderConfig:
base_url: str
api_key: str
rate_limit: RateLimitConfig
priority: int = 1
enabled: bool = True
class MultiModelRouter:
"""
Multi-Model Router สำหรับจัดการ quota และ rate limit
ของหลาย AI provider อย่างเป็นระบบ
"""
def __init__(self):
self.providers: Dict[Provider, ProviderConfig] = {}
self.quota_status: Dict[Provider, QuotaStatus] = {}
self.request_history: Dict[Provider, List[float]] = {}
self.fallback_chain: Dict[Provider, List[Provider]] = {}
def add_provider(
self,
provider: Provider,
api_key: str,
base_url: str,
rate_limit: Optional[RateLimitConfig] = None,
priority: int = 1
):
"""เพิ่ม provider เข้าสู่ระบบ router"""
if rate_limit is None:
rate_limit = RateLimitConfig()
self.providers[provider] = ProviderConfig(
base_url=base_url,
api_key=api_key,
rate_limit=rate_limit,
priority=priority
)
self.quota_status[provider] = QuotaStatus()
self.request_history[provider] = []
logger.info(f"Added provider: {provider.value} with base_url: {base_url}")
def set_fallback_chain(self, primary: Provider, fallbacks: List[Provider]):
"""กำหนด fallback chain เมื่อ primary provider ล่ม"""
self.fallback_chain[primary] = fallbacks
async def _check_rate_limit(self, provider: Provider) -> bool:
"""ตรวจสอบว่า request นี้ผ่าน rate limit หรือยัง"""
config = self.providers[provider]
history = self.request_history[provider]
now = time.time()
# ลบ request เก่ากว่า 1 นาที
self.request_history[provider] = [
t for t in history if now - t < 60
]
current_rpm = len(self.request_history[provider])
if current_rpm >= config.rate_limit.requests_per_minute:
logger.warning(
f"Rate limit exceeded for {provider.value}: "
f"{current_rpm}/{config.rate_limit.requests_per_minute} RPM"
)
return False
return True
async def _check_quota(self, provider: Provider, estimated_tokens: int) -> bool:
"""ตรวจสอบว่า quota เพียงพอหรือยัง"""
status = self.quota_status[provider]
if status.used_today + estimated_tokens >= status.limit_today:
logger.warning(
f"Quota exceeded for {provider.value}: "
f"{status.used_today}/{status.limit_today} tokens"
)
return False
return True
def update_quota_usage(self, provider: Provider, tokens_used: int):
"""อัพเดท quota usage หลังจาก request เสร็จ"""
self.quota_status[provider].used_today += tokens_used
self.quota_status[provider].remaining -= tokens_used
async def route_request(
self,
prompt: str,
model: str,
preferred_provider: Optional[Provider] = None,
estimated_tokens: int = 1000,
max_retries: int = 3
) -> dict:
"""
Route request ไปยัง provider ที่เหมาะสม
พร้อม fallback เมื่อ provider หลักล่ม
"""
if preferred_provider:
providers_to_try = [preferred_provider] + self.fallback_chain.get(preferred_provider, [])
else:
providers_to_try = sorted(
self.providers.keys(),
key=lambda p: self.providers[p].priority,
reverse=True
)
last_error = None
for provider in providers_to_try:
if not self.providers[provider].enabled:
continue
# ตรวจสอบ rate limit
if not await self._check_rate_limit(provider):
continue
# ตรวจสอบ quota
if not await self._check_quota(provider, estimated_tokens):
continue
# ลอง request
for attempt in range(max_retries):
try:
result = await self._make_request(provider, model, prompt)
# อัพเดท history
self.request_history[provider].append(time.time())
# อัพเดท quota
if 'usage' in result:
self.update_quota_usage(provider, result['usage'])
return {
'success': True,
'provider': provider.value,
'data': result
}
except Exception as e:
last_error = e
logger.error(
f"Request failed for {provider.value} "
f"(attempt {attempt + 1}/{max_retries}): {str(e)}"
)
if "rate limit" in str(e).lower() or "429" in str(e):
await asyncio.sleep(2 ** attempt) # Exponential backoff
continue
elif "401" in str(e) or "unauthorized" in str(e).lower():
# ไม่ retry กับ auth error
break
else:
continue
return {
'success': False,
'error': str(last_error),
'providers_tried': [p.value for p in providers_to_try]
}
async def _make_request(self, provider: Provider, model: str, prompt: str) -> dict:
"""ส่ง request ไปยัง provider"""
config = self.providers[provider]
async with httpx.AsyncClient(timeout=30.0) as client:
headers = {
"Authorization": f"Bearer {config.api_key}",
"Content-Type": "application/json"
}
if provider == Provider.ANTHROPIC:
response = await client.post(
f"{config.base_url}/messages",
headers={**headers, "x-api-key": config.api_key, "anthropic-version": "2023-06-01"},
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1024
}
)
elif provider == Provider.OPENAI:
response = await client.post(
f"{config.base_url}/chat/completions",
headers=headers,
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1024
}
)
elif provider == Provider.GEMINI:
response = await client.post(
f"{config.base_url}/models/{model}:generateContent",
headers=headers,
json={"contents": [{"parts": [{"text": prompt}]}]}
)
elif provider == Provider.DEEPSEEK:
response = await client.post(
f"{config.base_url}/chat/completions",
headers=headers,
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1024
}
)
if response.status_code != 200:
raise Exception(f"{response.status_code}: {response.text}")
return response.json()
def get_status(self) -> Dict[str, dict]:
"""ดึงสถานะทั้งหมดของ providers"""
status = {}
for provider in self.providers:
status[provider.value] = {
"enabled": self.providers[provider].enabled,
"quota_used": self.quota_status[provider].used_today,
"quota_limit": self.quota_status[provider].limit_today,
"remaining_rpm": (
self.providers[provider].rate_limit.requests_per_minute
- len(self.request_history[provider])
)
}
return status
ตัวอย่างการใช้งาน
async def main():
router = MultiModelRouter()
# เพิ่ม HolySheep AI เป็น unified gateway
router.add_provider(
Provider.ANTHROPIC,
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1/claude",
rate_limit=RateLimitConfig(requests_per_minute=120, tokens_per_minute=200000),
priority=1
)
router.add_provider(
Provider.OPENAI,
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1/openai",
rate_limit=RateLimitConfig(requests_per_minute=100, tokens_per_minute=150000),
priority=2
)
# ตั้งค่า fallback chain
router.set_fallback_chain(
Provider.ANTHROPIC,
[Provider.OPENAI, Provider.DEEPSEEK]
)
# ทดสอบ request
result = await router.route_request(
prompt="อธิบายเรื่อง REST API",
model="claude-sonnet-4-20250514",
estimated_tokens=500
)
print(f"Result: {result}")
if __name__ == "__main__":
asyncio.run(main())
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
จากประสบการณ์การใช้งาน multi-model router มานานหลายเดือน ผมรวบรวมข้อผิดพลาดที่พบบ่อยที่สุด 3 กรณี พร้อมวิธีแก้ไขที่ใช้ได้จริงใน production:
กรณีที่ 1: Rate Limit 429 - Too Many Requests
อาการ: ได้รับ error 429 บ่อยครั้ง โดยเฉพาะช่วง peak hours
import asyncio
from typing import Optional
import time
class SmartRateLimiter:
"""
Rate Limiter ที่ปรับตัวอัตโนมัติตาม response ของ server
"""
def __init__(self, initial_rpm: int = 60, safety_margin: float = 0.8):
self.current_rpm = initial_rpm
self.safety_margin = safety_margin
self.actual_limit = initial_rpm
self.backoff_multiplier = 1.0
self.retry_after = 60 # seconds
def calculate_safe_rpm(self) -> int:
"""คำนวณ RPM ที่ปลอดภัยให้ server"""
safe_rpm = min(self.current_rpm * self.safety_margin, self.actual_limit)
return int(safe_rpm * self.backoff_multiplier)
async def acquire(self, provider_name: str) -> bool:
"""
รอจนกว่าจะได้รับอนุญาตให้ส่ง request
"""
effective_rpm = self.calculate_safe_rpm()
# ตรวจสอบว่าเป็น cooldown period หรือไม่
if hasattr(self, 'cooldown_until') and time.time() < self.cooldown_until:
wait_time = self.cooldown_until - time.time()
print(f"[{provider_name}] Cooldown: รอ {wait_time:.1f} วินาที")
await asyncio.sleep(wait_time)
# ตรวจสอบ RPM
current_count = getattr(self, f'request_count_{provider_name}', 0)
if current_count >= effective_rpm:
wait_time = 60 - (time.time() % 60)
print(f"[{provider_name}] RPM limit: รอ {wait_time:.1f} วินาที")
await asyncio.sleep(wait_time)
setattr(self, f'request_count_{provider_name}', 0)
setattr(self, f'request_count_{provider_name}', current_count + 1)
return True
def handle_429(self, retry_after_header: Optional[int] = None):
"""
จัดการเมื่อเจอ 429 error
"""
# ลด RPM ลง 50%
self.current_rpm = int(self.current_rpm * 0.5)
self.backoff_multiplier *= 0.5
# ตั้ง cooldown period
cooldown_seconds = retry_after_header or self.retry_after
self.cooldown_until = time.time() + cooldown_seconds
print(f"[RateLimit] 429 received - RPM ลดเหลือ {self.current_rpm}, "
f"cooldown {cooldown_seconds} วินาที")
def handle_success(self):
"""
เมื่อ request สำเร็จ ค่อยๆ เพิ่ม RPM กลับ
"""
if self.backoff_multiplier < 1.0:
self.backoff_multiplier = min(self.backoff_multiplier * 1.1, 1.0)
print(f"[RateLimit] Recovery: backoff multiplier = {self.backoff_multiplier:.2f}")
วิธีใช้งาน
async def example_with_rate_limiter():
limiter = SmartRateLimiter(initial_rpm=100)
for i in range(150):
await limiter.acquire("openai")
# ส่ง request
try:
# response = await send_request(...)
print(f"Request {i+1} สำเร็จ")
limiter.handle_success()
except Exception as e:
if "429" in str(e):
retry_after = int(e.headers.get("Retry-After", 60))
limiter.handle_429(retry_after)
รันทดสอบ
asyncio.run(example_with_rate_limiter())
กรณีที่ 2: Authentication Error 401 - Invalid API Key
อาการ: ได้รับ 401 Unauthorized แม้ว่า API key จะถูกต้อง เกิดจาก key หมดอายุ หรือถูก revoke
import os
from datetime import datetime, timedelta
from typing import Dict, Optional
import json
import logging
logger = logging.getLogger(__name__)
class APIKeyManager:
"""
จัดการ API keys หลายตัว พร้อม auto-rotation และ health check
"""
def __init__(self, key_storage_path: str = "./keys.json"):
self.key_storage_path = key_storage_path
self.keys: Dict[str, dict] = {}
self.load_keys()
def load_keys(self):
"""โหลด keys จากไฟล์"""
if os.path.exists(self.key_storage_path):
with open(self.key_storage_path, 'r') as f:
self.keys = json.load(f)
logger.info(f"โหลด {len(self.keys)} API keys")
else:
# สร้าง demo keys
self.keys = {
"openai_primary": {
"key": "sk-xxx-primary",
"provider": "openai",
"created_at": datetime.now().isoformat(),
"last_used": None,
"failure_count": 0,
"status": "active"
},
"openai_secondary": {
"key": "sk-xxx-secondary",
"provider": "openai",
"created_at": datetime.now().isoformat(),
"last_used": None,
"failure_count": 0,
"status": "active"
}
}
self.save_keys()
def save_keys(self):
"""บันทึก keys ลงไฟล์"""
with open(self.key_storage_path, 'w') as f:
json.dump(self.keys, f, indent=2)
def get_key(self, provider: str) -> Optional[str]:
"""ดึง key ที่ใช้ได้สำหรับ provider นี้"""
available_keys = [
(name, data) for name, data in self.keys.items()
if data['provider'] == provider and data['status'] == 'active'
]
if not available_keys:
logger.warning(f"ไม่มี key ที่ใช้ได้สำหรับ {provider}")
return None
# เลือก key ที่ใช้ล่าสุดน้อยที่สุด (load balancing)
available_keys.sort(key=lambda x: x[1]['last_used'] or '0')
selected_name, selected_data = available_keys[0]
# อัพเดท last_used
selected_data['last_used'] = datetime.now().isoformat()
self.save_keys()
return selected_data['key']
def report_failure(self, provider: str):
"""รายงานว่า key มีปัญหา"""
for name, data in self.keys.items():
if data['provider'] == provider and data['status'] == 'active':
data['failure_count'] += 1
# ถ้ามี failure 3 ครั้งติด ให้ deactivate
if data['failure_count'] >= 3:
data['status'] = 'deactivated'
logger.warning(
f"Key {name} ถูก deactivate หลังจาก "
f"{data['failure_count']} failures"
)
self.save_keys()
def report_success(self, provider: str):
"""รายงานว่า request สำเร็จ - ลด failure count"""
for name, data in self.keys.items():
if data['provider'] == provider:
data['failure_count'] = max(0, data['failure_count'] - 1)
self.save_keys()
def add_key(self, name: str, api_key: str, provider: str):
"""เพิ่ม key ใหม่"""
self.keys[name] = {
"key": api_key,
"provider": provider,
"created_at": datetime.now().isoformat(),
"last_used": None,
"failure_count": 0,
"status": "active"
}
self.save_keys()
logger.info(f"เพิ่ม key ใหม่: {name} สำหรับ {provider}")
def health_check(self) -> Dict[str, dict]:
"""ตรวจสอบสถานะของทุก key"""
summary = {}
for name, data in self.keys.items():
summary[name] = {
"provider": data['provider'],
"status": data['status'],
"failure_count": data['failure_count'],
"last_used": data['last_used'],
"age_days": (
datetime.now() - datetime.fromisoformat(data['created_at'])
).days if data['created_at'] else None
}
return summary
วิธีใช้งานร่วมกับ MultiModelRouter
def create_authenticated_request(key_manager: APIKeyManager):
"""สร้าง request function ที่จัดการ auth อัตโนมัติ"""
async def authenticated_request(provider: str, endpoint: str, payload: dict):
api_key = key_manager.get_key(provider)
if not api_key:
raise Exception(f"ไม่มี API key ที่ใช้ได้สำหรับ {provider}")
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
# ส่ง request
# async with httpx.AsyncClient() as client:
# response = await client.post(endpoint, headers=headers, json=payload)
# ตรวจสอบ response
# if response.status_code == 401:
# key_manager.report_failure(provider)
# # retry ด้วย key ใหม่
# return await authenticated_request(provider, endpoint, payload)
# elif response.status_code == 200:
# key_manager.report_success(provider)
return {"status": "success", "provider": provider}
return authenticated_request
ตัวอย่างการใช้งาน
key_manager = APIKeyManager()
request_func = create_authenticated_request(key_manager)
เพิ่ม HolySheep key
key_manager.add_key(
"holysheep_unified",
"YOUR_HOLYSHEEP_API_KEY",
"holysheep"
)
print("Health Check:", key_manager.health_check())
กรณีที่ 3: Quota Exceeded - งบประมาณหมดก่อนเวลา
อาการ: ได้รับแจ้งว่า quota หมด ทั้งที่ยังไม่ถึงวันสิ้นเดือน มักเกิดจากการใช้งานที่ไม่คาดคิด
from dataclasses import dataclass
from typing import Dict, List
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
@dataclass
class QuotaAlert:
provider: str
used_percent: float
estimated_runout: datetime
severity: str # 'warning', 'critical', 'emergency'
class QuotaManager:
"""
จัดการ quota ของทุก provider พร้อมระบบแจ้งเตือน
"""
def __init__(
self,
daily_budget_usd: float = 100.0,
warning_threshold: float = 0.70,
critical_threshold: float = 0.90
):
self.daily_budget = daily_budget_usd
self.warning_threshold = warning_threshold
self.critical_threshold = critical_threshold
self.quota_limits: Dict[str, float] = {} # เป็น USD
self.current_spend: Dict[str, float] = {}
self.price_per_mtok: Dict[str, float] = {
# ราคาจาก HolySheep 2026 (ประหยัด 85%+ เมื่อเทียบกับ official)
"gpt-4.1": 8.0,
"claude-sonnet-4.5": 15.0,
"gemini-2.5-flash": 2.50,
"deepseek-v3.2": 0.42,
}
self.reset_daily()
def reset_daily(self):
"""รีเซ็ต spend counter รายวัน"""
self.current_spend = {provider: 0.0 for provider in self.price_per_mtok}
logger.info("Daily quota reset แล้ว")
def set_provider_limit(self, provider: str, monthly_limit_usd: float):
"""กำหนด monthly limit สำหรับ provider"""
self.quota_limits[provider] = monthly_limit_usd
logger.info(f"ตั้งค่า limit สำหรับ {provider}: ${monthly_limit_usd}/เดือน")
def estimate_cost(
self,
model: str,
input_tokens: int,
output_tokens: int
) -> float:
"""ประมาณค่าใช้จ่ายของ request (USD)"""
if model not in self.price_per_mtok:
logger.warning(f"ไม่มีราคาของ {model} -