Files changed (1) hide show
  1. runtime.py +242 -0
runtime.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tempfile
5
+ import time
6
+ from collections.abc import Iterator
7
+ from typing import Any, Callable
8
+
9
+ DEFAULT_PROVIDER_FALLBACK = (
10
+ "auto",
11
+ "hf-inference",
12
+ "novita",
13
+ "together",
14
+ "fireworks-ai",
15
+ )
16
+ PRO_PROVIDER_FALLBACK = (
17
+ "hf-inference",
18
+ "auto",
19
+ "novita",
20
+ "together",
21
+ "fireworks-ai",
22
+ )
23
+ MAX_INFERENCE_ATTEMPTS = 3
24
+ MAX_PRO_INFERENCE_ATTEMPTS = 2
25
+ MAX_PRO_PROVIDER_HOPS = 3
26
+ RETRY_BACKOFF_SECONDS = (0.75, 1.5, 3.0)
27
+ RETRYABLE_ERROR_MARKERS = (
28
+ "429",
29
+ "rate limit",
30
+ "502",
31
+ "503",
32
+ "504",
33
+ "timeout",
34
+ "timed out",
35
+ "temporarily",
36
+ "overloaded",
37
+ "unavailable",
38
+ "connection reset",
39
+ "connection aborted",
40
+ )
41
+
42
+ _pro_status_by_token: dict[str, bool] = {}
43
+
44
+
45
+ def detect_hosting() -> str:
46
+ if os.getenv("SPACE_ID"):
47
+ return "huggingface"
48
+ if os.getenv("CODESPACES") == "true":
49
+ return "github_codespaces"
50
+ if os.getenv("GITHUB_ACTIONS") == "true":
51
+ return "github_actions"
52
+ if os.getenv("PORT") or os.getenv("WEBSITES_PORT"):
53
+ return "container"
54
+ return "local"
55
+
56
+
57
+ def resolve_hf_token() -> str | None:
58
+ try:
59
+ from huggingface_hub import get_token
60
+
61
+ token = get_token()
62
+ if token:
63
+ return token
64
+ except Exception:
65
+ pass
66
+ return os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
67
+
68
+
69
+ def reset_hf_pro_cache() -> None:
70
+ _pro_status_by_token.clear()
71
+
72
+
73
+ def resolve_hf_pro_status(*, token: str | None = None) -> bool:
74
+ token = token or resolve_hf_token()
75
+ if not token:
76
+ return False
77
+ if token in _pro_status_by_token:
78
+ return _pro_status_by_token[token]
79
+ try:
80
+ from huggingface_hub import whoami
81
+
82
+ info = whoami(token=token, cache=True)
83
+ is_pro = bool(info.get("isPro"))
84
+ except Exception:
85
+ return False
86
+ _pro_status_by_token[token] = is_pro
87
+ return is_pro
88
+
89
+
90
+ def resolve_server_port() -> int | None:
91
+ for key in ("PORT", "WEBSITES_PORT", "GRADIO_SERVER_PORT", "SPACE_PORT"):
92
+ raw = os.getenv(key)
93
+ if not raw:
94
+ continue
95
+ try:
96
+ return int(raw)
97
+ except ValueError:
98
+ continue
99
+ return None
100
+
101
+
102
+ def gradio_launch_kwargs(*, css: str | None = None) -> dict[str, Any]:
103
+ hosting = detect_hosting()
104
+ kwargs: dict[str, Any] = {
105
+ "css": css,
106
+ "ssr_mode": False,
107
+ "show_error": True,
108
+ "allowed_paths": [tempfile.gettempdir()],
109
+ }
110
+
111
+ if hosting in {"huggingface", "github_codespaces", "container"}:
112
+ kwargs["server_name"] = "0.0.0.0"
113
+
114
+ port = resolve_server_port()
115
+ if port is not None:
116
+ kwargs["server_port"] = port
117
+
118
+ root_path = os.getenv("GRADIO_ROOT_PATH")
119
+ if root_path:
120
+ kwargs["root_path"] = root_path
121
+
122
+ return kwargs
123
+
124
+
125
+ def inference_provider_chain(preferred: str | None, *, is_pro: bool = False) -> list[str]:
126
+ preferred_value = (preferred or "auto").strip() or "auto"
127
+ fallback = PRO_PROVIDER_FALLBACK if is_pro else DEFAULT_PROVIDER_FALLBACK
128
+
129
+ if is_pro and preferred_value == "auto":
130
+ chain = list(PRO_PROVIDER_FALLBACK)
131
+ else:
132
+ chain = [preferred_value]
133
+ for provider in fallback:
134
+ if provider not in chain:
135
+ chain.append(provider)
136
+
137
+ if is_pro:
138
+ return chain[:MAX_PRO_PROVIDER_HOPS]
139
+ return chain
140
+
141
+
142
+ def inference_attempts_per_provider(*, is_pro: bool = False) -> int:
143
+ return MAX_PRO_INFERENCE_ATTEMPTS if is_pro else MAX_INFERENCE_ATTEMPTS
144
+
145
+
146
+ def inference_routing_hint() -> str:
147
+ token = resolve_hf_token()
148
+ if not token:
149
+ return ""
150
+ if resolve_hf_pro_status(token=token):
151
+ return (
152
+ "<span style='color:#69ff53;font-size:0.92em'>"
153
+ "HF Pro routing active — hf-inference preferred.</span>"
154
+ )
155
+ return (
156
+ "<span style='color:#9fdfff;font-size:0.92em'>"
157
+ "Standard Hugging Face inference routing.</span>"
158
+ )
159
+
160
+
161
+ def is_retryable_inference_error(exc: Exception) -> bool:
162
+ message = str(exc).lower()
163
+ return any(marker in message for marker in RETRYABLE_ERROR_MARKERS)
164
+
165
+
166
+ def stream_chat_completion(
167
+ client_factory: Callable[[str], Any],
168
+ *,
169
+ providers: list[str],
170
+ model: str,
171
+ messages: list[dict[str, str]],
172
+ max_tokens: int,
173
+ temperature: float,
174
+ max_attempts_per_provider: int = MAX_INFERENCE_ATTEMPTS,
175
+ ) -> Iterator[str]:
176
+ last_error: Exception | None = None
177
+
178
+ for provider in providers:
179
+ for attempt in range(max_attempts_per_provider):
180
+ try:
181
+ client = client_factory(provider)
182
+ stream = client.chat_completion(
183
+ model=model,
184
+ messages=messages,
185
+ max_tokens=max_tokens,
186
+ temperature=temperature,
187
+ stream=True,
188
+ )
189
+ for chunk in stream:
190
+ delta = chunk.choices[0].delta.content or ""
191
+ if delta:
192
+ yield delta
193
+ return
194
+ except Exception as exc:
195
+ last_error = exc
196
+ if (
197
+ attempt + 1 >= max_attempts_per_provider
198
+ or not is_retryable_inference_error(exc)
199
+ ):
200
+ break
201
+ time.sleep(RETRY_BACKOFF_SECONDS[min(attempt, len(RETRY_BACKOFF_SECONDS) - 1)])
202
+
203
+ if last_error is not None:
204
+ raise last_error
205
+ raise RuntimeError("Inference failed without a provider response.")
206
+
207
+
208
+ def format_inference_failure(
209
+ exc: Exception,
210
+ providers: list[str],
211
+ *,
212
+ is_pro: bool = False,
213
+ ) -> str:
214
+ hosting = detect_hosting()
215
+ routing = "HF Pro routing (hf-inference preferred)" if is_pro else "standard routing"
216
+ lines = [
217
+ f"Inference failed after trying providers: {', '.join(providers)} ({routing}).",
218
+ f"Last error: {exc}",
219
+ ]
220
+ if hosting == "huggingface":
221
+ lines.append(
222
+ "On Hugging Face Spaces, add an HF token with Inference Providers permission "
223
+ "as the HF_TOKEN secret."
224
+ )
225
+ elif hosting.startswith("github"):
226
+ lines.append(
227
+ "On GitHub-hosted runtimes, set HF_TOKEN (or HUGGING_FACE_HUB_TOKEN) in repository "
228
+ "or Codespace secrets."
229
+ )
230
+ else:
231
+ lines.append(
232
+ "Set HF_TOKEN locally or choose a different inference provider/model."
233
+ )
234
+ if is_pro:
235
+ lines.append(
236
+ "HF Pro is active; if errors persist, try a specific provider or model."
237
+ )
238
+ lines.append(
239
+ "Transient provider outages are retried automatically; persistent errors need "
240
+ "a new token, model, or provider."
241
+ )
242
+ return "\n\n".join(lines)