Spaces:
Running
Running
File size: 8,208 Bytes
1f69928 | 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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | """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"),
"groq": ("GROQ", "https://api.groq.com/openai/v1", "qwen/qwen3.8-27b"),
}
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 free_only():
"""Deployment policy; Groq additionally requires the operator's Free plan."""
return os.getenv("INFERENCE_FREE_ONLY", "false").strip().lower() in {"1", "true", "yes"}
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
if free_only():
groq_free = (
choice == "groq" and base_url == "https://api.groq.com/openai/v1"
and model in {"qwen/qwen3.8-27b", "openai/gpt-oss-20b", "openai/gpt-oss-120b"}
)
router_free = (
choice == "openrouter-free" and base_url == "https://openrouter.ai/api/v1"
and model == "openrouter/free"
)
if not (groq_free or router_free):
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):
payload = {
"model": route["model"],
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3,
"max_tokens": 768,
}
if route["url"].startswith("https://api.groq.com/") and route["model"] == "qwen/qwen3.8-27b":
payload["reasoning_effort"] = "none"
body = json.dumps(payload).encode("utf-8")
request = urllib.request.Request(route["url"], data=body, headers={
"Authorization": "Bearer " + route["key"],
"Content-Type": "application/json",
"User-Agent": "PureDialectApps/1.0",
}, 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]
if free_only() and choice == "auto":
names = ["groq", "openrouter-free"]
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 == 401:
result = failure("PROVIDER_AUTH_FAILED", "The selected provider rejected its credentials.")
elif exc.code == 403:
result = failure("PROVIDER_ACCESS_DENIED", "The provider denied access. Check its model and project permissions.")
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}
|