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}