Nullpointer-KK commited on
Commit
c911920
·
verified ·
1 Parent(s): b288188

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +271 -69
app.py CHANGED
@@ -1,108 +1,310 @@
1
  import os
2
  import hmac
 
 
3
  import gradio as gr
 
 
 
 
 
 
4
  from openai import OpenAI
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- def _resolve_openai_key(user_input: str) -> str | None:
8
  """
9
- If `user_input` is an 8-digit code matching BACKDOOR_KEY (Space secret),
10
- return the OPENAI_KEY (Space secret). Otherwise return `user_input`.
11
- Never prints or returns secrets; just resolves which to use.
12
  """
13
- user_input = (user_input or "").strip()
 
 
 
 
 
14
 
15
- backdoor_code = (os.getenv("BACKDOOR_KEY") or "").strip()
16
- # Only treat as backdoor attempt if it's exactly 8 digits and we have a BACKDOOR_KEY.
17
- is_candidate_code = user_input.isdigit() and len(user_input) == 8 and backdoor_code
18
 
19
- if is_candidate_code and hmac.compare_digest(user_input, backdoor_code):
20
- # Use the real key stored as a Space secret
21
- resolved = os.getenv("OPENAI_KEY")
22
- if not resolved:
23
- # Fail safe if OPENAI_KEY isn't configured
24
- return None
25
- return resolved
26
 
27
- # Fallback to whatever the user typed (normal behavior)
28
- return user_input if user_input else None
 
 
29
 
 
 
 
30
 
31
- def complete_text(prompt, max_tokens, temperature, top_p, openai_api_key_input):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  """
33
- Get a plain text completion from OpenAI.
34
- Supports a secret 8-digit passcode in the API key field that swaps in the
35
- Space secret OPENAI_KEY when it matches BACKDOOR_KEY.
36
  """
37
- resolved_key = _resolve_openai_key(openai_api_key_input)
38
- if not resolved_key:
39
- return "⚠️ Missing or invalid API key. (Tip: set OPENAI_KEY/BACKDOOR_KEY in Space secrets, or paste a valid key.)"
 
 
 
 
 
 
 
 
 
40
 
 
41
  try:
42
- client = OpenAI(api_key=resolved_key)
 
 
 
 
 
 
 
 
 
 
43
 
44
- response_text = ""
45
- stream = client.completions.create(
46
- # NOTE: gpt-3.5-turbo-instruct is legacy. Consider switching to a current instruct-capable model.
47
- model="gpt-3.5-turbo-instruct",
48
- prompt=prompt,
49
- max_tokens=int(max_tokens),
50
- temperature=float(temperature),
51
- top_p=float(top_p),
52
- stream=True,
53
- )
54
-
55
- for event in stream:
56
- if hasattr(event, "choices") and event.choices:
57
- token = event.choices[0].text or ""
58
  response_text += token
59
  yield response_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  except Exception as e:
62
- # Don't leak secrets or full tracebacks to the UI.
63
- yield f"❌ Error while generating completion: {type(e).__name__}. Check your model name, key, or params."
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  with gr.Blocks() as demo:
67
- gr.Markdown("## ✍️ Text Completion Demo (OpenAI instruct)")
68
  gr.Markdown(
69
- "Enter a prompt, adjust decoding parameters, and watch the model complete your text."
 
70
  )
71
 
72
  with gr.Row():
73
- with gr.Column(scale=2):
74
- prompt = gr.Textbox(
75
- label="Prompt",
76
- placeholder="Type the beginning of your text...",
77
- lines=4,
78
- )
79
- max_tokens = gr.Slider(
80
- minimum=1, maximum=1024, value=100, step=1, label="Max tokens"
81
  )
82
- temperature = gr.Slider(
83
- minimum=0.0, maximum=2.0, value=0.7, step=0.1, label="Temperature"
 
 
84
  )
85
- top_p = gr.Slider(
86
- minimum=0.1, maximum=1.0, value=1.0, step=0.05, label="Top-p"
87
- )
88
- api_key = gr.Textbox(
89
- placeholder="sk-... Paste your OpenAI API key here (or enter 8-digit passcode)",
90
- label="🔑 OpenAI API Key",
91
- type="password",
92
- )
93
- submit = gr.Button("Generate Completion")
94
- with gr.Column(scale=3):
95
- output = gr.Textbox(
96
- label="Generated Completion",
97
- lines=15,
98
  )
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  submit.click(
101
- fn=complete_text,
102
- inputs=[prompt, max_tokens, temperature, top_p, api_key],
 
 
 
 
103
  outputs=output,
104
  )
105
 
106
  if __name__ == "__main__":
107
- # On Spaces, server_name='0.0.0.0' helps; share=False by default.
108
  demo.launch()
 
1
  import os
2
  import hmac
3
+ from typing import Iterable, Generator, Optional, Dict, Any
4
+
5
  import gradio as gr
6
+
7
+ # --- Optional: install SDKs in your Space (add to requirements.txt) ---
8
+ # openai>=1.40.0
9
+ # google-genai>=0.3.0
10
+
11
+ # OpenAI-compatible SDK (used for OpenAI and DeepSeek)
12
  from openai import OpenAI
13
 
14
+ # Gemini SDK
15
+ try:
16
+ from google import genai
17
+ from google.genai import types as genai_types
18
+ except ImportError:
19
+ genai = None
20
+ genai_types = None
21
+
22
+
23
+ # -------- Helpers: secret/backdoor resolution (optional pattern you already like) --------
24
+ def _timing_safe_eq(a: str, b: str) -> bool:
25
+ return hmac.compare_digest(a, b)
26
+
27
 
28
+ def _resolve_key(user_value: str, secret_gate_name: str, secret_payload_name: str) -> Optional[str]:
29
  """
30
+ If user_value is an 8-digit code that matches ENV[secret_gate_name], return ENV[secret_payload_name].
31
+ Else return user_value (or None if empty).
 
32
  """
33
+ user_value = (user_value or "").strip()
34
+ backdoor_code = (os.getenv(secret_gate_name) or "").strip()
35
+ if user_value.isdigit() and len(user_value) == 8 and backdoor_code:
36
+ if _timing_safe_eq(user_value, backdoor_code):
37
+ return (os.getenv(secret_payload_name) or "").strip() or None
38
+ return user_value or None
39
 
 
 
 
40
 
41
+ # -------- Providers --------
42
+ OPENAI_MODELS = [
43
+ "gpt-4o",
44
+ "o4-mini", # aka gpt-4o-mini family; see OpenAI docs
45
+ "gpt-3.5-turbo",
46
+ ]
 
47
 
48
+ GEMINI_MODELS = [
49
+ "gemini-1.5-flash",
50
+ "gemini-2.0-flash", # available via Gemini API / Vertex; keep synced with docs
51
+ ]
52
 
53
+ DEEPSEEK_MODELS = [
54
+ "deepseek-chat",
55
+ ]
56
 
57
+ PROVIDERS = {
58
+ "OpenAI": OPENAI_MODELS,
59
+ "Gemini": GEMINI_MODELS,
60
+ "DeepSeek": DEEPSEEK_MODELS,
61
+ }
62
+
63
+
64
+ # -------- Streaming runners per provider --------
65
+ def stream_openai_like(
66
+ model: str,
67
+ prompt: str,
68
+ temperature: float,
69
+ top_p: float,
70
+ max_tokens: int,
71
+ seed: Optional[int],
72
+ api_key: str,
73
+ base_url: Optional[str] = None,
74
+ ) -> Generator[str, None, None]:
75
  """
76
+ Streams Chat Completions from OpenAI or any OpenAI-compatible endpoint (DeepSeek).
 
 
77
  """
78
+ client = OpenAI(api_key=api_key, base_url=base_url) if base_url else OpenAI(api_key=api_key)
79
+ # Chat format even for simple prompting
80
+ kwargs: Dict[str, Any] = dict(
81
+ model=model,
82
+ messages=[{"role": "user", "content": prompt}],
83
+ temperature=temperature,
84
+ top_p=top_p,
85
+ max_tokens=max_tokens,
86
+ stream=True,
87
+ )
88
+ if seed is not None:
89
+ kwargs["seed"] = seed # supported in recent OpenAI SDKs
90
 
91
+ response_text = ""
92
  try:
93
+ stream = client.chat.completions.create(**kwargs)
94
+ for part in stream:
95
+ delta = part.choices[0].delta if hasattr(part.choices[0], "delta") else None
96
+ # Some SDKs use .delta, some have .message or .text in streaming chunks
97
+ token = ""
98
+ if delta and getattr(delta, "content", None):
99
+ token = delta.content
100
+ elif hasattr(part.choices[0], "message") and part.choices[0].message.content:
101
+ token = part.choices[0].message.content
102
+ elif hasattr(part.choices[0], "text") and part.choices[0].text: # fallback
103
+ token = part.choices[0].text
104
 
105
+ if token:
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  response_text += token
107
  yield response_text
108
+ except Exception as e:
109
+ yield f"❌ OpenAI-compatible error: {type(e).__name__}: {e}"
110
+
111
+
112
+ def stream_gemini(
113
+ model: str,
114
+ prompt: str,
115
+ temperature: float,
116
+ top_p: float,
117
+ max_tokens: int,
118
+ seed: Optional[int],
119
+ api_key: str,
120
+ ) -> Generator[str, None, None]:
121
+ """
122
+ Streams from Google Gemini via google.genai SDK.
123
+ """
124
+ if genai is None:
125
+ yield "❌ Gemini SDK not installed. Add `google-genai` to requirements.txt."
126
+ return
127
+
128
+ client = genai.Client(api_key=api_key)
129
+
130
+ # Build generation config (Gemini supports these fields; seed is optional)
131
+ gen_cfg = {
132
+ "temperature": float(temperature),
133
+ "top_p": float(top_p),
134
+ "max_output_tokens": int(max_tokens),
135
+ }
136
+ if seed is not None:
137
+ gen_cfg["seed"] = int(seed)
138
+
139
+ response_text = ""
140
+ try:
141
+ with client.responses.stream(
142
+ model=model,
143
+ input=prompt,
144
+ config=genai_types.GenerateContentConfig(
145
+ temperature=gen_cfg["temperature"],
146
+ top_p=gen_cfg["top_p"],
147
+ max_output_tokens=gen_cfg["max_output_tokens"],
148
+ seed=gen_cfg.get("seed"),
149
+ ),
150
+ ) as stream:
151
+ for event in stream:
152
+ # Each event may carry incremental text in candidates[0].content.parts[…].text
153
+ try:
154
+ for cand in getattr(event, "candidates", []) or []:
155
+ parts = getattr(cand, "content", None)
156
+ if parts and getattr(parts, "parts", None):
157
+ for p in parts.parts:
158
+ txt = getattr(p, "text", None)
159
+ if txt:
160
+ response_text += txt
161
+ yield response_text
162
+ except Exception:
163
+ # Best-effort incremental parse
164
+ pass
165
+
166
+ # Ensure final text is yielded (for some drivers, the last event is summary)
167
+ final = getattr(stream, "text", None)
168
+ if final and final not in response_text:
169
+ response_text += final
170
+ yield response_text
171
 
172
  except Exception as e:
173
+ yield f"❌ Gemini error: {type(e).__name__}: {e}"
174
+
175
 
176
+ # -------- Gradio callback --------
177
+ def multi_llm_complete(
178
+ provider: str,
179
+ model: str,
180
+ prompt: str,
181
+ max_tokens: int,
182
+ temperature: float,
183
+ top_p: float,
184
+ seed_text: str,
185
+ # API keys (user enters). You can also support the 8-digit backdoor pattern per provider:
186
+ openai_key_input: str,
187
+ gemini_key_input: str,
188
+ deepseek_key_input: str,
189
+ ):
190
+ # Resolve seed
191
+ seed: Optional[int] = None
192
+ if seed_text and str(seed_text).strip().isdigit():
193
+ seed = int(str(seed_text).strip())
194
 
195
+ # Resolve keys (optionally allow an 8-digit backdoor per provider)
196
+ if provider == "OpenAI":
197
+ api_key = _resolve_key(openai_key_input, "OPENAI_BACKDOOR_KEY", "OPENAI_KEY") or ""
198
+ if not api_key:
199
+ yield "⚠️ Enter a valid OpenAI API key."
200
+ return
201
+ # Stream via OpenAI
202
+ for chunk in stream_openai_like(
203
+ model=model,
204
+ prompt=prompt,
205
+ temperature=temperature,
206
+ top_p=top_p,
207
+ max_tokens=max_tokens,
208
+ seed=seed,
209
+ api_key=api_key,
210
+ base_url=None,
211
+ ):
212
+ yield chunk
213
+
214
+ elif provider == "Gemini":
215
+ api_key = _resolve_key(gemini_key_input, "GEMINI_BACKDOOR_KEY", "GEMINI_KEY") or ""
216
+ if not api_key:
217
+ yield "⚠️ Enter a valid Gemini API key."
218
+ return
219
+ for chunk in stream_gemini(
220
+ model=model,
221
+ prompt=prompt,
222
+ temperature=temperature,
223
+ top_p=top_p,
224
+ max_tokens=max_tokens,
225
+ seed=seed,
226
+ api_key=api_key,
227
+ ):
228
+ yield chunk
229
+
230
+ elif provider == "DeepSeek":
231
+ api_key = _resolve_key(deepseek_key_input, "DEEPSEEK_BACKDOOR_KEY", "DEEPSEEK_KEY") or ""
232
+ if not api_key:
233
+ yield "⚠️ Enter a valid DeepSeek API key."
234
+ return
235
+ # DeepSeek: OpenAI-compatible endpoint
236
+ for chunk in stream_openai_like(
237
+ model=model,
238
+ prompt=prompt,
239
+ temperature=temperature,
240
+ top_p=top_p,
241
+ max_tokens=max_tokens,
242
+ seed=seed,
243
+ api_key=api_key,
244
+ base_url="https://api.deepseek.com",
245
+ ):
246
+ yield chunk
247
+
248
+ else:
249
+ yield "❌ Unknown provider selection."
250
+
251
+
252
+ # -------- UI --------
253
  with gr.Blocks() as demo:
254
+ gr.Markdown("## 🔀 Multi-LLM Chat (OpenAI • Gemini • DeepSeek)")
255
  gr.Markdown(
256
+ "Pick a provider & model, enter the provider’s API key, tune params, and stream the reply. "
257
+ "Seed (if supported) improves reproducibility."
258
  )
259
 
260
  with gr.Row():
261
+ with gr.Column(scale=1):
262
+ provider = gr.Dropdown(
263
+ choices=list(PROVIDERS.keys()),
264
+ value="OpenAI",
265
+ label="Provider",
 
 
 
266
  )
267
+ model = gr.Dropdown(
268
+ choices=PROVIDERS["OpenAI"],
269
+ value="gpt-4o",
270
+ label="Model",
271
  )
272
+
273
+ def _update_models(p):
274
+ return gr.update(choices=PROVIDERS[p], value=PROVIDERS[p][0])
275
+
276
+ provider.change(_update_models, inputs=provider, outputs=model)
277
+
278
+ prompt = gr.Textbox(
279
+ label="Prompt",
280
+ placeholder="Ask anything…",
281
+ lines=6,
 
 
 
282
  )
283
 
284
+ max_tokens = gr.Slider(1, 4096, value=512, step=1, label="Max tokens")
285
+ temperature = gr.Slider(0.0, 2.0, value=0.7, step=0.1, label="Temperature")
286
+ top_p = gr.Slider(0.0, 1.0, value=1.0, step=0.01, label="Top-p")
287
+ seed = gr.Textbox(label="🎲 Seed (optional integer)", placeholder="e.g., 42")
288
+
289
+ with gr.Column(scale=1):
290
+ gr.Markdown("### API Keys (per provider)")
291
+ openai_key = gr.Textbox(label="OpenAI API Key", type="password", placeholder="sk-... or 8-digit passcode")
292
+ gemini_key = gr.Textbox(label="Gemini API Key", type="password", placeholder="AI Studio key or 8-digit passcode")
293
+ deepseek_key = gr.Textbox(label="DeepSeek API Key", type="password", placeholder="ds-... or 8-digit passcode")
294
+
295
+ submit = gr.Button("▶️ Generate", variant="primary")
296
+ output = gr.Textbox(label="Response", lines=18)
297
+
298
  submit.click(
299
+ fn=multi_llm_complete,
300
+ inputs=[
301
+ provider, model, prompt,
302
+ max_tokens, temperature, top_p, seed,
303
+ openai_key, gemini_key, deepseek_key
304
+ ],
305
  outputs=output,
306
  )
307
 
308
  if __name__ == "__main__":
309
+ # On Spaces, consider server_name='0.0.0.0'
310
  demo.launch()