Download fallback.py from kswffs/sentinel: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/spaces/kswffs/sentinel/resolve/main/fallback.py
- Command line
-
hf download hf://spaces/kswffs/sentinel/fallback.py
-
curl -L -o fallback.py https://huggingface.co/spaces/kswffs/sentinel/resolve/main/fallback.py
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)}"} | |