Spaces:
Sleeping
Sleeping
File size: 7,033 Bytes
946166e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | """Small, fail-closed provider adapter shared by REST and Gradio.
Configured routes are not health claims. Explicit model choices never fall back
to another provider; ``auto`` may choose from configured routes.
"""
import json
import os
import re
import time
import urllib.error
import urllib.parse
import urllib.request
ROUTES = {
"qwen": ("QWEN", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "qwen-plus"),
"llama": ("GROQ", "https://api.groq.com/openai/v1", "llama-3.3-70b-versatile"),
"deepseek": ("DEEPSEEK", "https://api.deepseek.com", "deepseek-chat"),
"gemini": ("GEMINI", "https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.5-flash"),
"nemotron": ("OPENROUTER", "https://openrouter.ai/api/v1", "nvidia/nemotron-3-nano-30b-a3b:free"),
"gpt-oss": ("OPENROUTER", "https://openrouter.ai/api/v1", "openai/gpt-oss-20b:free"),
"lfm": ("OPENROUTER", "https://openrouter.ai/api/v1", "liquid/lfm-2.5-1.2b-instruct:free"),
"openrouter-free": ("OPENROUTER", "https://openrouter.ai/api/v1", "openrouter/free"),
}
MODEL_ENV = {
"nemotron": "OPENROUTER_NEMOTRON_MODEL_NAME",
"gpt-oss": "OPENROUTER_GPT_OSS_MODEL_NAME",
"lfm": "OPENROUTER_LFM_MODEL_NAME",
"openrouter-free": "OPENROUTER_FREE_MODEL_NAME",
}
ERROR_TEXT = re.compile(
r"^(?:AI[_ ]GENERATION[_ ]FAILED|AI generation failed|Transcription (?:failed|unavailable)|"
r"Translation (?:failed|unavailable)|Error\s*:|Service unavailable)", re.I
)
def failure(code, message):
return {"ok": False, "error": {"code": code, "message": message}}
def valid_content(text):
return isinstance(text, str) and 0 < len(text.strip()) <= 4000 and not ERROR_TEXT.search(text.strip())
def _route(choice):
prefix, default_url, default_model = ROUTES[choice]
key = os.getenv(prefix + "_API_KEY", "").strip()
if prefix == "GEMINI":
key = key or os.getenv("GOOGLE_API_KEY", "").strip()
if not key or key.lower() in {"your-api-key-here", "none", "null"}:
return None
model = os.getenv(MODEL_ENV.get(choice, prefix + "_MODEL_NAME"), default_model).strip()
base_url = os.getenv(prefix + "_BASE_URL", default_url).strip().rstrip("/")
parsed = urllib.parse.urlsplit(base_url)
# Only operator-configured endpoints are used. Never put keys in URLs.
if parsed.scheme not in {"https", "http"} or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
return None
if parsed.scheme == "http" and parsed.hostname not in {"localhost", "127.0.0.1", "::1"}:
return None
if not model:
return None
return {"key": key, "url": base_url + "/chat/completions", "model": model, "route": choice}
def available_routes():
"""Return non-secret configuration metadata, without claiming availability."""
return [
{"route": name, "model": route["model"], "configured": True, "verified": False}
for name in ROUTES if (route := _route(name))
]
class _NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
# Do not forward Authorization to a redirected host.
return None
class _OutputFailure(Exception):
def __init__(self, code, message):
super().__init__(message)
self.code = code
self.message = message
def _post(route, prompt, timeout=10):
body = json.dumps({
"model": route["model"],
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3,
"max_tokens": 768,
}).encode("utf-8")
request = urllib.request.Request(route["url"], data=body, headers={
"Authorization": "Bearer " + route["key"],
"Content-Type": "application/json",
}, method="POST")
with urllib.request.build_opener(_NoRedirect).open(request, timeout=timeout) as response:
raw = response.read(1024 * 1024 + 1)
if len(raw) > 1024 * 1024:
raise ValueError("Response exceeded limit")
payload = json.loads(raw)
choice = payload["choices"][0]
finish_reason = choice.get("finish_reason")
if finish_reason in {"length", "max_tokens"}:
raise _OutputFailure("OUTPUT_TRUNCATED", "The model response was cut short. Shorten the text or retry.")
if finish_reason != "stop" or choice.get("message", {}).get("tool_calls"):
raise _OutputFailure("INVALID_OUTPUT", "The model did not return a complete usable response.")
return choice["message"]["content"]
def generate_text(prompt, model_choice="auto", *, deadline=None):
if not isinstance(prompt, str) or not prompt.strip() or len(prompt) > 24000:
return failure("INVALID_REQUEST", "A non-empty, bounded inference prompt is required.")
choice = str(model_choice or "auto").strip().lower()
if choice != "auto" and choice not in ROUTES:
return failure("MODEL_UNAVAILABLE", "The selected model route is not supported.")
names = list(ROUTES) if choice == "auto" else [choice]
configured = [route for name in names if (route := _route(name))]
if not configured:
return failure("MODEL_UNAVAILABLE", "The selected model route is not configured.")
deadline = min(deadline or (time.monotonic() + 30), time.monotonic() + 30)
attempts = []
result = failure("PROVIDER_UNAVAILABLE", "The selected provider is unavailable. Please retry.")
for route in configured[:3]:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
try:
content = _post(route, prompt, timeout=min(10, remaining))
if valid_content(content):
return {"ok": True, "text": content.strip(), "model": route["model"], "route": route["route"], "attempts": attempts}
result = failure("INVALID_OUTPUT", "The model did not return usable content.")
except _OutputFailure as exc:
result = failure(exc.code, exc.message)
except urllib.error.HTTPError as exc:
if exc.code in {401, 403}:
result = failure("PROVIDER_AUTH_FAILED", "The selected provider rejected its credentials.")
elif exc.code == 429:
result = failure("PROVIDER_RATE_LIMITED", "The selected provider is rate limited or has no remaining quota.")
elif exc.code in {400, 404, 422}:
result = failure("PROVIDER_CONFIGURATION_ERROR", "The selected provider rejected the configured model or request.")
else:
result = failure("PROVIDER_UNAVAILABLE", "The selected provider is unavailable. Please retry.")
except (TimeoutError, urllib.error.URLError):
result = failure("PROVIDER_UNAVAILABLE", "The selected provider could not be reached. Please retry.")
except Exception:
result = failure("INVALID_OUTPUT", "The model did not return usable content.")
attempts.append({"route": route["route"], "code": result["error"]["code"]})
return {**result, "attempts": attempts}
|