YugJ commited on
Commit
5355966
·
verified ·
1 Parent(s): 25d9f62

Upload providers.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. providers.py +386 -0
providers.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model registry, client construction, and the key policy for public hosting.
2
+
3
+ Every provider here speaks the OpenAI wire format, so one client class covers
4
+ all of them and only the base URL changes.
5
+
6
+ Key policy, in priority order per request:
7
+
8
+ 1. A key the visitor typed into the UI ("bring your own key"). Used for that
9
+ single request and then dropped - never logged, never persisted, never
10
+ written to disk.
11
+ 2. Otherwise the host's own key for that provider, if one is configured.
12
+ On the public deployment that is deliberately a free-tier provider, and
13
+ it is rate limited per session.
14
+
15
+ The demo defaults to Groq's free tier serving gpt-oss-120b - which is the model
16
+ that actually won this project's benchmark, so the zero-cost path is also the
17
+ one that shows the best result.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import time
24
+ from dataclasses import dataclass, field
25
+
26
+ from openai import OpenAI
27
+
28
+ # --------------------------------------------------------------------------
29
+ # registry
30
+ # --------------------------------------------------------------------------
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Provider:
35
+ key_env: str # environment variable holding the key
36
+ base_url: str | None # None means the OpenAI default endpoint
37
+ label: str
38
+ signup: str = ""
39
+ needs_key: bool = True
40
+
41
+
42
+ PROVIDERS: dict[str, Provider] = {
43
+ "openai": Provider("OPENAI_API_KEY", None, "OpenAI",
44
+ "https://platform.openai.com/api-keys"),
45
+ "anthropic": Provider("ANTHROPIC_API_KEY", "https://api.anthropic.com/v1/", "Anthropic",
46
+ "https://console.anthropic.com/settings/keys"),
47
+ "google": Provider("GOOGLE_API_KEY", "https://generativelanguage.googleapis.com/v1beta/openai/", "Google",
48
+ "https://aistudio.google.com/apikey"),
49
+ "grok": Provider("GROK_API_KEY", "https://api.x.ai/v1", "xAI",
50
+ "https://console.x.ai"),
51
+ "groq": Provider("GROQ_API_KEY", "https://api.groq.com/openai/v1", "Groq (free tier)",
52
+ "https://console.groq.com/keys"),
53
+ "openrouter": Provider("OPENROUTER_API_KEY", "https://openrouter.ai/api/v1", "OpenRouter",
54
+ "https://openrouter.ai/keys"),
55
+ "ollama": Provider("", os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1"),
56
+ "Ollama (local)", "https://ollama.com", needs_key=False),
57
+ }
58
+
59
+
60
+ DEMO = "demo" # cheap or free - the host's key may serve these
61
+ BYOK = "byok" # expensive - the visitor must supply their own key
62
+ LOCAL = "local" # runs on the machine hosting the app, costs nothing
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Model:
67
+ id: str
68
+ provider: str
69
+ display: str
70
+ note: str = ""
71
+ reasoning: bool = False # accepts the reasoning_effort parameter
72
+ tier: str = BYOK # default to the safe side: never spend the host's money
73
+
74
+
75
+ # Tiering is based on measured results, not list price alone. On the max-subarray
76
+ # benchmark, gpt-5-nano produced a correct translation running 62x faster than
77
+ # Python; GPT-5 with high reasoning effort scored 63x. The difference is noise,
78
+ # and the cost difference is not - so the small model is the demo default and
79
+ # the frontier models are opt-in with the visitor's own key.
80
+ MODELS: list[Model] = [
81
+ # -- demo tier: safe to serve with the host's key --------------------
82
+ Model("openai/gpt-oss-120b", "groq", "GPT-OSS 120B",
83
+ "free tier - fastest result in this benchmark", tier=DEMO),
84
+ Model("gpt-5-nano", "openai", "GPT-5 Nano",
85
+ "very low cost - scored 62x on the benchmark", reasoning=True, tier=DEMO),
86
+ Model("gpt-5-mini", "openai", "GPT-5 Mini",
87
+ "low cost - scored 63x", reasoning=True, tier=DEMO),
88
+
89
+ # -- frontier: visitor's own key only --------------------------------
90
+ Model("gpt-5", "openai", "GPT-5", "frontier", reasoning=True, tier=BYOK),
91
+ Model("claude-sonnet-5", "anthropic", "Claude Sonnet 5", "frontier", tier=BYOK),
92
+ Model("grok-4", "grok", "Grok 4", "frontier", tier=BYOK),
93
+ Model("qwen/qwen3-coder-30b-a3b-instruct", "openrouter", "Qwen3 Coder 30B",
94
+ "open weights", tier=BYOK),
95
+ # Flash models failed to compile on both benchmarks tried - they emit Python
96
+ # format strings such as {:.6f}, which is not valid Rust. Kept for comparison
97
+ # because that failure is itself an interesting result, but not defaulted to.
98
+ Model("gemini-3.5-flash", "google", "Gemini 3.5 Flash",
99
+ "fast, but failed to compile in testing", tier=BYOK),
100
+
101
+ # -- local via Ollama -------------------------------------------------
102
+ Model("gpt-oss:20b", "ollama", "GPT-OSS 20B", "runs locally", tier=LOCAL),
103
+ Model("qwen2.5-coder", "ollama", "Qwen2.5 Coder", "runs locally", tier=LOCAL),
104
+ Model("deepseek-coder-v2", "ollama", "DeepSeek Coder V2", "runs locally", tier=LOCAL),
105
+ ]
106
+
107
+ BY_ID = {m.id: m for m in MODELS}
108
+
109
+ DEMO_MODEL = os.getenv("DEMO_MODEL", "openai/gpt-oss-120b")
110
+
111
+
112
+ # --------------------------------------------------------------------------
113
+ # availability
114
+ # --------------------------------------------------------------------------
115
+
116
+
117
+ _ollama_cache: tuple[float, bool] | None = None
118
+
119
+
120
+ def ollama_reachable() -> bool:
121
+ """Whether a local Ollama server is actually answering.
122
+
123
+ Ollama needs no key, so a key check would report its models as ready even
124
+ with nothing running - and the visitor would get a connection error after
125
+ waiting. Probing the socket keeps the availability badges honest. Cached
126
+ briefly so it does not run on every dropdown render.
127
+ """
128
+ global _ollama_cache
129
+ now = time.time()
130
+ if _ollama_cache and now - _ollama_cache[0] < 30:
131
+ return _ollama_cache[1]
132
+
133
+ import urllib.error
134
+ import urllib.request
135
+
136
+ base = PROVIDERS["ollama"].base_url or ""
137
+ url = base.rstrip("/").removesuffix("/v1") + "/api/tags"
138
+ try:
139
+ with urllib.request.urlopen(url, timeout=1.5) as response:
140
+ alive = response.status == 200
141
+ except Exception:
142
+ alive = False
143
+
144
+ _ollama_cache = (now, alive)
145
+ return alive
146
+
147
+
148
+ def host_key_for(provider: str) -> str | None:
149
+ """The host's configured key for a provider, if any."""
150
+ spec = PROVIDERS[provider]
151
+ if not spec.needs_key:
152
+ return "not-needed" if ollama_reachable() else None
153
+ value = os.getenv(spec.key_env, "").strip()
154
+ return value or None
155
+
156
+
157
+ def may_use_host_key(model: Model) -> bool:
158
+ """Whether this model is allowed to spend the owner's credits.
159
+
160
+ Frontier models never are - not on the public demo and not locally. The
161
+ measured benefit over gpt-5-nano on this task is nil (62x vs 63x), so
162
+ charging frontier prices for it is pure waste, and an accidental click
163
+ while developing costs just as much as a visitor's.
164
+ """
165
+ if model.tier == LOCAL:
166
+ return True # costs nothing, runs on this machine
167
+ return model.tier == DEMO
168
+
169
+
170
+ def visible_models() -> list[Model]:
171
+ """Models worth showing on this instance.
172
+
173
+ Local models are hidden unless Ollama is actually answering. The probe runs
174
+ on the *server*, so a hosted container will never find one - and listing a
175
+ permanently broken option is worse than not listing it.
176
+ """
177
+ models = [m for m in MODELS if m.tier != LOCAL]
178
+ if not PUBLIC and ollama_reachable():
179
+ models += [m for m in MODELS if m.tier == LOCAL]
180
+ return models
181
+
182
+
183
+ def is_available(model_id: str) -> bool:
184
+ """True when the host can serve this model with no key from the visitor."""
185
+ model = BY_ID.get(model_id)
186
+ if not model or not may_use_host_key(model):
187
+ return False
188
+ return host_key_for(model.provider) is not None
189
+
190
+
191
+ def needs_own_key(model_id: str) -> bool:
192
+ """True when this model requires the visitor to supply a key."""
193
+ model = BY_ID.get(model_id)
194
+ if not model or model.tier == LOCAL:
195
+ return False
196
+ return not is_available(model_id)
197
+
198
+
199
+ def choice_label(model: Model) -> str:
200
+ """Dropdown label. The cost warning goes first, where it cannot be missed.
201
+
202
+ Trailing markers get truncated in a narrow select element - exactly the
203
+ situation where someone is about to spend money without meaning to.
204
+ """
205
+ note = f" - {model.note}" if model.note else ""
206
+
207
+ if is_available(model.id) or model.tier == LOCAL:
208
+ return f"{model.display}{note}"
209
+ if model.tier == DEMO and not PUBLIC:
210
+ # Local dev: the owner can fix this, and naming the variable is the
211
+ # fastest route. Suppressed in public mode - a visitor cannot set an
212
+ # environment variable on someone else's server, so it would only
213
+ # confuse them. There, the honest instruction is the same as frontier.
214
+ return f"(set {PROVIDERS[model.provider].key_env}) {model.display}{note}"
215
+ return f"(need your own key) {model.display}{note}"
216
+
217
+
218
+ def dropdown_choices() -> list[tuple[str, str]]:
219
+ """(label, value) pairs, ready models first."""
220
+ models = visible_models()
221
+ ordered = sorted(models, key=lambda m: (not is_available(m.id), models.index(m)))
222
+ return [(choice_label(m), m.id) for m in ordered]
223
+
224
+
225
+ def default_model() -> str:
226
+ """Prefer the configured demo model, then any ready model, then anything."""
227
+ models = visible_models()
228
+ if is_available(DEMO_MODEL):
229
+ return DEMO_MODEL
230
+ for m in models:
231
+ if m.tier == DEMO and is_available(m.id):
232
+ return m.id
233
+ for m in models:
234
+ if is_available(m.id):
235
+ return m.id
236
+ return models[0].id
237
+
238
+
239
+ def availability_summary() -> str:
240
+ """Short markdown line describing what the running instance can serve."""
241
+ models = visible_models()
242
+ ready = [m for m in models if is_available(m.id)]
243
+ if not ready:
244
+ return (
245
+ "**No models ready.** Add a provider key to `.env` "
246
+ "(see `.env.example`), or paste your own key below to try it now."
247
+ )
248
+ names = ", ".join(m.display for m in ready[:3])
249
+ extra = f" +{len(ready) - 3} more" if len(ready) > 3 else ""
250
+ tail = ""
251
+ if PUBLIC:
252
+ tail = " Frontier models need your own key - they are too costly to give away."
253
+ return f"**Ready now:** {names}{extra}.{tail}"
254
+
255
+
256
+ # --------------------------------------------------------------------------
257
+ # rate limiting
258
+ # --------------------------------------------------------------------------
259
+
260
+
261
+ @dataclass
262
+ class RateLimiter:
263
+ """Per-session cap on requests that spend the *host's* key.
264
+
265
+ Requests carrying a visitor's own key are never counted - they cost the
266
+ host nothing, so throttling them would only punish the considerate.
267
+
268
+ In-memory and per-process, which is the right size for a single-container
269
+ demo. A multi-replica deployment would need shared state.
270
+ """
271
+
272
+ limit: int = field(default_factory=lambda: int(os.getenv("DEMO_RATE_LIMIT", "5")))
273
+ window_seconds: int = 3600
274
+ _hits: dict[str, list[float]] = field(default_factory=dict)
275
+
276
+ def check(self, session_id: str) -> tuple[bool, str]:
277
+ if self.limit <= 0:
278
+ return True, ""
279
+ now = time.time()
280
+ recent = [t for t in self._hits.get(session_id, []) if now - t < self.window_seconds]
281
+ self._hits[session_id] = recent
282
+ if len(recent) >= self.limit:
283
+ wait = int((self.window_seconds - (now - recent[0])) / 60) + 1
284
+ return False, (
285
+ f"Demo limit reached ({self.limit} free conversions per hour).\n\n"
286
+ f"Try again in ~{wait} minutes, or open **Use your own API key** "
287
+ f"below for unlimited use with your own account."
288
+ )
289
+ return True, ""
290
+
291
+ def record(self, session_id: str) -> None:
292
+ self._hits.setdefault(session_id, []).append(time.time())
293
+
294
+ def remaining(self, session_id: str) -> int:
295
+ if self.limit <= 0:
296
+ return -1
297
+ now = time.time()
298
+ recent = [t for t in self._hits.get(session_id, []) if now - t < self.window_seconds]
299
+ return max(0, self.limit - len(recent))
300
+
301
+
302
+ limiter = RateLimiter()
303
+ PUBLIC = os.getenv("PUBLIC_DEPLOYMENT", "0") == "1"
304
+
305
+
306
+ # --------------------------------------------------------------------------
307
+ # client construction
308
+ # --------------------------------------------------------------------------
309
+
310
+
311
+ class NoKeyError(RuntimeError):
312
+ """Raised when neither the visitor nor the host can supply a usable key."""
313
+
314
+
315
+ def build_client(model_id: str, user_key: str | None) -> tuple[OpenAI, bool]:
316
+ """Return (client, used_host_key).
317
+
318
+ `user_key` is used for this call only. It is never stored, logged, or
319
+ written anywhere - it lives in this frame and is released with it.
320
+ """
321
+ model = BY_ID.get(model_id)
322
+ if model is None:
323
+ raise NoKeyError(f"Unknown model: {model_id}")
324
+
325
+ spec = PROVIDERS[model.provider]
326
+ user_key = (user_key or "").strip()
327
+
328
+ if user_key:
329
+ key, used_host = user_key, False
330
+ elif not may_use_host_key(model):
331
+ # Frontier tier. Refuse before the host key is even read, rather than
332
+ # reading it and checking a limit afterwards.
333
+ raise NoKeyError(
334
+ f"**{model.display}** is a frontier model and never runs on this "
335
+ f"instance's key - a few clicks is real money.\n\n"
336
+ f"Open **Use your own API key** below and paste a {spec.label} key "
337
+ f"({spec.signup}) to use it.\n\n"
338
+ f"Or pick a model without the warning: on this benchmark GPT-5 Nano "
339
+ f"scored 62x against GPT-5's 63x, so you give up essentially nothing."
340
+ )
341
+ else:
342
+ host = host_key_for(model.provider)
343
+ if host is None:
344
+ if not spec.needs_key:
345
+ raise NoKeyError(
346
+ f"**{model.display}** runs through a local Ollama server, and "
347
+ f"none is responding at {spec.base_url}.\n\n"
348
+ f"Start it with `ollama serve` and `ollama pull {model.id}`, "
349
+ f"or choose a hosted model instead. Pasting an API key will "
350
+ f"not help with a local model."
351
+ )
352
+ raise NoKeyError(
353
+ f"**{model.display}** needs a {spec.label} key, and this instance "
354
+ f"has none configured.\n\n"
355
+ f"Open **Use your own API key** below and paste a {spec.label} key "
356
+ f"({spec.signup}), or pick a model marked as ready."
357
+ )
358
+ key, used_host = host, True
359
+
360
+ if not spec.needs_key:
361
+ return OpenAI(api_key="ollama", base_url=spec.base_url), used_host
362
+ if spec.base_url:
363
+ return OpenAI(api_key=key, base_url=spec.base_url), used_host
364
+ return OpenAI(api_key=key), used_host
365
+
366
+
367
+ def scrub(text: str) -> str:
368
+ """Strip anything key-shaped out of text before it reaches the UI.
369
+
370
+ Provider SDKs sometimes echo the offending credential back inside an error
371
+ message. Since those messages get rendered into the page, they pass
372
+ through here first.
373
+ """
374
+ import re
375
+
376
+ patterns = [
377
+ r"sk-[A-Za-z0-9_\-]{12,}",
378
+ r"gsk_[A-Za-z0-9_\-]{12,}",
379
+ r"xai-[A-Za-z0-9_\-]{12,}",
380
+ r"AIza[A-Za-z0-9_\-]{20,}",
381
+ r"sk-or-v1-[A-Za-z0-9_\-]{12,}",
382
+ r"pplx-[A-Za-z0-9_\-]{12,}",
383
+ ]
384
+ for pattern in patterns:
385
+ text = re.sub(pattern, "[redacted]", text)
386
+ return text