sentinel / fallback.py
kswffs's picture
Upload folder using huggingface_hub
b96103d verified
Raw
History Blame Contribute Delete
12.1 kB
import asyncio
import base64
import httpx
import structlog
logger = structlog.get_logger()
class FallbackRouter:
"""
Asynchronous fallback routing engine for Sentinel.
Provides backup API inference paths (OpenRouter, OpenBMB, OpenAI)
when local edge/GPU Modal execution fails or times out.
"""
def __init__(self, openrouter_key: str, openbmb_key: str, openai_key: str):
"""
Initializes the FallbackRouter with required keys.
"""
self.openrouter_key = openrouter_key
self.openbmb_key = openbmb_key
self.openai_key = openai_key
# Initialize an AsyncClient with a 30s timeout
self.client = httpx.AsyncClient(timeout=30.0)
async def close(self):
"""
Closes the HTTP client session.
"""
await self.client.aclose()
async def _handle_http_error(self, provider_name: str, error: Exception) -> bool:
"""
Helper method to handle exceptions.
If a rate limit (HTTP 429) is encountered, waits 2 seconds.
Returns True if a retry/fallback should continue, False otherwise.
"""
if isinstance(error, httpx.HTTPStatusError):
status_code = error.response.status_code
logger.warn(
"HTTP Error during fallback call",
provider=provider_name,
status_code=status_code,
error=str(error)
)
if status_code == 429:
logger.info("Rate limit (429) hit. Backing off for 2.0s...", provider=provider_name)
await asyncio.sleep(2.0)
elif isinstance(error, httpx.TimeoutException):
logger.warn("Request timed out during fallback call", provider=provider_name, error=str(error))
else:
logger.error("Unexpected error during fallback call", provider=provider_name, error=str(error))
return True
async def fallback_reason(self, prompt: str, system_prompt: str = "") -> dict:
"""
Executes text reasoning using fallback models.
Primary: OpenRouter (nvidia/nemotron-3-nano-30b-a3b:free) — 30B MoE, free tier
Secondary: OpenAI (gpt-4o-mini)
"""
fallback_chain = []
# 1. Try OpenRouter (Nemotron-3-Nano-30B-A3B MoE, free tier)
if self.openrouter_key:
fallback_chain.append("openrouter")
try:
logger.info("Attempting reasoning fallback via OpenRouter (Nemotron-30B-A3B)...")
headers = {
"Authorization": f"Bearer {self.openrouter_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://huggingface.co/spaces",
"X-Title": "Sentinel Safety Guardian"
}
payload = {
"model": "nvidia/nemotron-3-nano-30b-a3b:free",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"max_tokens": 512,
"temperature": 0.4
}
response = await self.client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
result = response.json()
text = result["choices"][0]["message"]["content"]
tokens = result.get("usage", {}).get("total_tokens", 0)
logger.info("OpenRouter reasoning fallback successful", model="nemotron-3-nano-30b-a3b:free")
return {
"text": text,
"tokens": tokens,
"model": "openrouter:nemotron-3-nano-30b-a3b:free",
"fallback_chain": fallback_chain
}
except Exception as e:
await self._handle_http_error("openrouter", e)
else:
logger.warn("OpenRouter API key missing. Skipping to next fallback.")
# 2. Try OpenAI (gpt-4o-mini)
if self.openai_key:
fallback_chain.append("openai")
try:
logger.info("Attempting reasoning fallback via OpenAI (gpt-4o-mini)...")
headers = {
"Authorization": f"Bearer {self.openai_key}",
"Content-Type": "application/json"
}
payload = {
"model": "gpt-4o-mini",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
}
response = await self.client.post(
"https://api.openai.com/v1/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
result = response.json()
text = result["choices"][0]["message"]["content"]
tokens = result.get("usage", {}).get("total_tokens", 0)
logger.info("OpenAI reasoning fallback successful")
return {
"text": text,
"tokens": tokens,
"model": "openai:gpt-4o-mini",
"fallback_chain": fallback_chain
}
except Exception as e:
await self._handle_http_error("openai", e)
else:
logger.warn("OpenAI API key missing. Cannot execute backup reasoning fallback.")
# If all providers fail
logger.error("All reasoning fallbacks failed.")
return {
"error": "all_providers_failed",
"text": "I'm having trouble thinking right now, please try again in a moment.",
"fallback_chain": fallback_chain
}
async def fallback_see(self, image_base64: str, question: str) -> dict:
"""
Executes vision-language inference using fallback models.
Primary: OpenBMB API (MiniCPM-V-4.6-Instruct)
Secondary: OpenAI (gpt-4o-mini)
"""
fallback_chain = []
# Ensure correct prefix for base64 image data URI
if not image_base64.startswith("data:"):
image_data_uri = f"data:image/jpeg;base64,{image_base64}"
else:
image_data_uri = image_base64
# 1. Try OpenBMB (ModelBest API)
if self.openbmb_key:
fallback_chain.append("openbmb")
try:
logger.info("Attempting vision fallback via OpenBMB (ModelBest)...")
headers = {
"Authorization": f"Bearer {self.openbmb_key}",
"Content-Type": "application/json"
}
payload = {
"model": "MiniCPM-V-4.6-Instruct",
"messages": [
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": image_data_uri}},
{"type": "text", "text": question}
]
}
]
}
response = await self.client.post(
"https://api.modelbest.cn/v1/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
result = response.json()
text = result["choices"][0]["message"]["content"]
tokens = result.get("usage", {}).get("total_tokens", 0)
logger.info("OpenBMB vision fallback successful")
return {
"text": text,
"tokens": tokens,
"model": "openbmb:minicpm-v-4.6",
"fallback_chain": fallback_chain
}
except Exception as e:
await self._handle_http_error("openbmb", e)
else:
logger.warn("OpenBMB API key missing. Skipping to next fallback.")
# 2. Try OpenAI (gpt-4o-mini vision)
if self.openai_key:
fallback_chain.append("openai")
try:
logger.info("Attempting vision fallback via OpenAI (gpt-4o-mini)...")
headers = {
"Authorization": f"Bearer {self.openai_key}",
"Content-Type": "application/json"
}
payload = {
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": image_data_uri}}
]
}
]
}
response = await self.client.post(
"https://api.openai.com/v1/chat/completions",
headers=headers,
json=payload
)
response.raise_for_status()
result = response.json()
text = result["choices"][0]["message"]["content"]
tokens = result.get("usage", {}).get("total_tokens", 0)
logger.info("OpenAI vision fallback successful")
return {
"text": text,
"tokens": tokens,
"model": "openai:gpt-4o-mini-vision",
"fallback_chain": fallback_chain
}
except Exception as e:
await self._handle_http_error("openai", e)
else:
logger.warn("OpenAI API key missing. Cannot execute backup vision fallback.")
# If all providers fail
logger.error("All vision fallbacks failed.")
return {
"error": "all_providers_failed",
"text": "I'm having trouble seeing right now, please check my connection and try again.",
"fallback_chain": fallback_chain
}
async def fallback_tts(self, text: str) -> dict:
"""
Synthesizes warning announcement audio using OpenAI TTS API.
Model: tts-1, Voice: alloy
"""
if not self.openai_key:
logger.error("Cannot perform TTS fallback: OpenAI API key is missing.")
return {"error": "openai_key_missing", "text": "OpenAI API key missing."}
try:
logger.info("Attempting TTS fallback via OpenAI...")
headers = {
"Authorization": f"Bearer {self.openai_key}",
"Content-Type": "application/json"
}
payload = {
"model": "tts-1",
"input": text,
"voice": "alloy"
}
response = await self.client.post(
"https://api.openai.com/v1/audio/speech",
headers=headers,
json=payload
)
response.raise_for_status()
audio_bytes = response.content
audio_base64 = base64.b64encode(audio_bytes).decode('utf-8')
logger.info("OpenAI TTS fallback successful")
return {
"audio_base64": audio_base64,
"model": "openai-tts-1"
}
except Exception as e:
logger.error("OpenAI TTS fallback failed", error=str(e))
return {"error": f"TTS fallback failed: {str(e)}"}