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}