Wimmboo commited on
Commit
139fc91
·
1 Parent(s): e4fb81c

deploy to HF Spaces: add Dockerfile, remove dashboard

Browse files
Files changed (3) hide show
  1. Dockerfile +7 -0
  2. proxy/router.py +4 -19
  3. static/index.html +0 -536
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ EXPOSE 7860
7
+ CMD uvicorn main:app --host 0.0.0.0 --port 7860
proxy/router.py CHANGED
@@ -3,11 +3,10 @@ import logging
3
  from typing import Any
4
 
5
  from fastapi import APIRouter, Depends, FastAPI, Request
6
- from fastapi.responses import HTMLResponse, JSONResponse, Response
7
- from fastapi.staticfiles import StaticFiles
8
  from starlette.exceptions import HTTPException as StarletteHTTPException
9
 
10
- from config import HOST, PORT, PROVIDER_KEYS, ROOT
11
  from proxy.auth import get_real_key, resolve_provider
12
  from proxy.client import forward_request
13
  from proxy.providers import PROVIDERS
@@ -24,9 +23,11 @@ MODELS = {
24
  "moonshotai/kimi-k2.6",
25
  "moonshotai/kimi-k2.5",
26
  "google/gemma-4-31b-it",
 
27
  "google/gemma-3-27b-it",
28
  ],
29
  "google": [
 
30
  "gemini-2.0-flash-001",
31
  "gemini-2.0-flash-lite-001",
32
  "gemini-1.5-pro-001",
@@ -94,26 +95,10 @@ async def proxy_keys() -> JSONResponse:
94
  return JSONResponse(masked)
95
 
96
 
97
- def _dashboard_html() -> str:
98
- try:
99
- return (ROOT / "static" / "index.html").read_text(encoding="utf-8")
100
- except FileNotFoundError:
101
- return "<h1>Dashboard not built yet</h1>"
102
-
103
-
104
  def create_app() -> FastAPI:
105
  app = FastAPI(title="OpenAI-Compatible API Proxy", version="1.0.0")
106
-
107
- static_dir = ROOT / "static"
108
- if static_dir.exists():
109
- app.mount("/static", StaticFiles(directory=static_dir), name="static")
110
-
111
  app.include_router(api)
112
 
113
- @app.get("/", response_class=HTMLResponse)
114
- async def dashboard() -> str:
115
- return _dashboard_html()
116
-
117
  @app.exception_handler(StarletteHTTPException)
118
  async def http_exception_handler(_request: Request, exc: StarletteHTTPException) -> JSONResponse:
119
  return JSONResponse(status_code=exc.status_code, content={"error": {"message": exc.detail, "type": "proxy_error"}})
 
3
  from typing import Any
4
 
5
  from fastapi import APIRouter, Depends, FastAPI, Request
6
+ from fastapi.responses import JSONResponse, Response
 
7
  from starlette.exceptions import HTTPException as StarletteHTTPException
8
 
9
+ from config import HOST, PORT, PROVIDER_KEYS
10
  from proxy.auth import get_real_key, resolve_provider
11
  from proxy.client import forward_request
12
  from proxy.providers import PROVIDERS
 
23
  "moonshotai/kimi-k2.6",
24
  "moonshotai/kimi-k2.5",
25
  "google/gemma-4-31b-it",
26
+ "google/gemma-4-26b-it",
27
  "google/gemma-3-27b-it",
28
  ],
29
  "google": [
30
+ "gemini-2.5-pro-001",
31
  "gemini-2.0-flash-001",
32
  "gemini-2.0-flash-lite-001",
33
  "gemini-1.5-pro-001",
 
95
  return JSONResponse(masked)
96
 
97
 
 
 
 
 
 
 
 
98
  def create_app() -> FastAPI:
99
  app = FastAPI(title="OpenAI-Compatible API Proxy", version="1.0.0")
 
 
 
 
 
100
  app.include_router(api)
101
 
 
 
 
 
102
  @app.exception_handler(StarletteHTTPException)
103
  async def http_exception_handler(_request: Request, exc: StarletteHTTPException) -> JSONResponse:
104
  return JSONResponse(status_code=exc.status_code, content={"error": {"message": exc.detail, "type": "proxy_error"}})
static/index.html DELETED
@@ -1,536 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>API Proxy</title>
7
- <style>
8
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
- html { -webkit-font-smoothing: antialiased; }
10
-
11
- :root {
12
- --bg: oklch(0.08 0 0);
13
- --surface: oklch(0.11 0 0);
14
- --surface-2: oklch(0.15 0 0);
15
- --ink: oklch(0.94 0 0);
16
- --muted: oklch(0.58 0 0);
17
- --faint: oklch(0.32 0 0);
18
- --border: oklch(0.20 0 0);
19
- --primary: oklch(0.68 0.13 38);
20
- --primary-text: oklch(1 0 0);
21
- --accent: oklch(0.68 0.12 175);
22
- --warning: oklch(0.74 0.12 85);
23
- --danger: oklch(0.62 0.16 25);
24
- }
25
-
26
- body {
27
- background: var(--bg);
28
- color: var(--ink);
29
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
30
- font-size: 14px;
31
- line-height: 1.5;
32
- }
33
-
34
- .page {
35
- max-width: 680px;
36
- margin: 0 auto;
37
- padding: 48px 24px 80px;
38
- }
39
-
40
- /* --- Section headers --- */
41
- .section-header {
42
- display: flex;
43
- align-items: center;
44
- gap: 10px;
45
- margin-bottom: 20px;
46
- }
47
- .section-label {
48
- font-size: 11px;
49
- font-weight: 600;
50
- letter-spacing: 0.06em;
51
- text-transform: uppercase;
52
- color: var(--muted);
53
- }
54
-
55
- /* --- Hero --- */
56
- .hero {
57
- padding: 36px 0 48px;
58
- }
59
- .hero-status {
60
- display: inline-flex;
61
- align-items: center;
62
- gap: 8px;
63
- font-size: 13px;
64
- font-weight: 500;
65
- padding: 6px 14px;
66
- border-radius: 20px;
67
- margin-bottom: 24px;
68
- background: var(--surface);
69
- border: 1px solid var(--border);
70
- transition: background 150ms ease;
71
- }
72
- .hero-status .dot {
73
- width: 8px;
74
- height: 8px;
75
- border-radius: 50%;
76
- background: var(--accent);
77
- }
78
- .hero-status .dot.offline { background: var(--danger); }
79
- .hero-status .dot.checking { background: var(--warning); }
80
- .hero-title {
81
- font-size: 28px;
82
- font-weight: 600;
83
- letter-spacing: -0.02em;
84
- line-height: 1.2;
85
- margin-bottom: 10px;
86
- }
87
- .hero-sub {
88
- font-size: 14px;
89
- color: var(--muted);
90
- max-width: 48ch;
91
- }
92
-
93
- /* --- Endpoint --- */
94
- .endpoint {
95
- display: flex;
96
- align-items: center;
97
- gap: 10px;
98
- margin-top: 28px;
99
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
100
- font-size: 13px;
101
- padding: 12px 16px;
102
- background: var(--surface);
103
- border: 1px solid var(--border);
104
- border-radius: 8px;
105
- word-break: break-all;
106
- }
107
- .endpoint .method {
108
- font-weight: 600;
109
- color: var(--primary);
110
- font-size: 11px;
111
- letter-spacing: 0.04em;
112
- flex-shrink: 0;
113
- }
114
- .endpoint .path {
115
- color: var(--ink);
116
- opacity: 0.85;
117
- }
118
- .endpoint .copy-btn {
119
- margin-left: auto;
120
- flex-shrink: 0;
121
- background: var(--surface-2);
122
- border: 1px solid var(--border);
123
- color: var(--muted);
124
- cursor: pointer;
125
- font-size: 11px;
126
- font-weight: 500;
127
- padding: 4px 10px;
128
- border-radius: 5px;
129
- font-family: system-ui, sans-serif;
130
- transition: background 150ms ease, color 150ms ease;
131
- }
132
- .endpoint .copy-btn:hover {
133
- background: var(--faint);
134
- color: var(--ink);
135
- }
136
- .endpoint .copy-btn.copied {
137
- color: var(--accent);
138
- }
139
-
140
- /* --- Provider cards --- */
141
- .provider-grid {
142
- display: flex;
143
- flex-direction: column;
144
- gap: 10px;
145
- }
146
- .provider-card {
147
- background: var(--surface);
148
- border: 1px solid var(--border);
149
- border-radius: 10px;
150
- padding: 18px 20px;
151
- display: flex;
152
- align-items: flex-start;
153
- justify-content: space-between;
154
- gap: 16px;
155
- transition: background 150ms ease, border-color 150ms ease;
156
- }
157
- .provider-card:hover {
158
- background: var(--surface-2);
159
- border-color: var(--faint);
160
- }
161
- .provider-left {
162
- display: flex;
163
- flex-direction: column;
164
- gap: 6px;
165
- min-width: 0;
166
- }
167
- .provider-name {
168
- font-size: 15px;
169
- font-weight: 600;
170
- letter-spacing: -0.01em;
171
- }
172
- .provider-models {
173
- font-size: 12px;
174
- color: var(--muted);
175
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
176
- word-break: break-word;
177
- line-height: 1.6;
178
- }
179
- .provider-right {
180
- display: flex;
181
- flex-direction: column;
182
- align-items: flex-end;
183
- gap: 6px;
184
- flex-shrink: 0;
185
- }
186
- .provider-status {
187
- font-size: 11px;
188
- font-weight: 600;
189
- letter-spacing: 0.04em;
190
- text-transform: uppercase;
191
- padding: 4px 10px;
192
- border-radius: 12px;
193
- background: var(--surface-2);
194
- color: var(--muted);
195
- border: 1px solid var(--border);
196
- }
197
- .provider-status.configured {
198
- color: var(--accent);
199
- border-color: oklch(0.30 0.05 175);
200
- background: oklch(0.15 0.02 175);
201
- }
202
-
203
- /* --- Key row --- */
204
- .key-section {
205
- display: flex;
206
- flex-direction: column;
207
- gap: 8px;
208
- }
209
- .key-row {
210
- display: flex;
211
- align-items: center;
212
- gap: 10px;
213
- background: var(--surface);
214
- border: 1px solid var(--border);
215
- border-radius: 8px;
216
- padding: 12px 14px;
217
- }
218
- .key-provider-label {
219
- font-size: 12px;
220
- font-weight: 600;
221
- letter-spacing: 0.03em;
222
- text-transform: uppercase;
223
- color: var(--muted);
224
- min-width: 72px;
225
- flex-shrink: 0;
226
- }
227
- .key-value {
228
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
229
- font-size: 13px;
230
- color: var(--ink);
231
- opacity: 0.7;
232
- min-width: 0;
233
- overflow: hidden;
234
- text-overflow: ellipsis;
235
- white-space: nowrap;
236
- user-select: all;
237
- }
238
- .key-value.masked {
239
- opacity: 0.4;
240
- filter: blur(4px);
241
- transition: filter 200ms ease, opacity 200ms ease;
242
- user-select: none;
243
- }
244
- .key-toggle {
245
- flex-shrink: 0;
246
- background: none;
247
- border: 1px solid var(--border);
248
- color: var(--muted);
249
- cursor: pointer;
250
- font-size: 11px;
251
- font-weight: 500;
252
- padding: 4px 10px;
253
- border-radius: 5px;
254
- font-family: system-ui, sans-serif;
255
- transition: background 150ms ease, color 150ms ease;
256
- }
257
- .key-toggle:hover { background: var(--surface-2); color: var(--ink); }
258
-
259
- /* --- Code blocks --- */
260
- .code-section {
261
- margin-top: 48px;
262
- }
263
- .code-block {
264
- background: var(--surface);
265
- border: 1px solid var(--border);
266
- border-radius: 10px;
267
- padding: 16px 18px;
268
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
269
- font-size: 13px;
270
- line-height: 1.7;
271
- color: var(--ink);
272
- overflow-x: auto;
273
- position: relative;
274
- margin-bottom: 10px;
275
- }
276
- .code-block .c { color: var(--muted); }
277
- .code-block .k { color: var(--primary); }
278
- .code-block .v { color: var(--accent); }
279
- .code-copy {
280
- position: absolute;
281
- top: 10px;
282
- right: 10px;
283
- background: var(--surface-2);
284
- border: 1px solid var(--border);
285
- color: var(--muted);
286
- cursor: pointer;
287
- font-size: 11px;
288
- font-weight: 500;
289
- padding: 4px 10px;
290
- border-radius: 5px;
291
- font-family: system-ui, sans-serif;
292
- transition: background 150ms ease, color 150ms ease;
293
- }
294
- .code-copy:hover { background: var(--faint); color: var(--ink); }
295
- .code-copy.copied { color: var(--accent); }
296
-
297
- /* --- Divider --- */
298
- .divider {
299
- border: none;
300
- border-top: 1px solid var(--border);
301
- margin: 40px 0;
302
- }
303
-
304
- /* --- Responsive --- */
305
- @media (max-width: 520px) {
306
- .page { padding: 32px 16px 60px; }
307
- .hero-title { font-size: 24px; }
308
- .provider-card {
309
- flex-direction: column;
310
- align-items: flex-start;
311
- }
312
- .provider-right {
313
- flex-direction: row;
314
- width: 100%;
315
- }
316
- .key-row { flex-wrap: wrap; }
317
- }
318
-
319
- @media (prefers-reduced-motion: reduce) {
320
- *, *::before, *::after { transition: none !important; }
321
- }
322
- </style>
323
- </head>
324
- <body>
325
- <div class="page">
326
-
327
- <!-- Hero -->
328
- <header class="hero">
329
- <div class="hero-status" id="status-badge">
330
- <span class="dot checking"></span>
331
- <span id="status-text">checking</span>
332
- </div>
333
- <h1 class="hero-title">API Proxy</h1>
334
- <p class="hero-sub">
335
- A personal gateway that routes OpenAI-compatible chat requests to NVIDIA NIM and Google AI Studio — based on which <code>sk-</code> key your app uses.
336
- </p>
337
- <div class="endpoint">
338
- <span class="method">POST</span>
339
- <span class="path" id="endpoint-url">http://localhost:8000/v1/chat/completions</span>
340
- <button class="copy-btn" data-copy="endpoint-url">Copy URL</button>
341
- </div>
342
- </header>
343
-
344
- <!-- Providers -->
345
- <section>
346
- <div class="section-header">
347
- <span class="section-label">Providers</span>
348
- </div>
349
- <div class="provider-grid" id="provider-list">
350
- <div class="provider-card" data-provider="nvidia">
351
- <div class="provider-left">
352
- <span class="provider-name">NVIDIA NIM</span>
353
- <span class="provider-models">deepseek-ai/deepseek-v4-flash, moonshotai/kimi-k2.6, google/gemma-4-31b-it</span>
354
- </div>
355
- <div class="provider-right">
356
- <span class="provider-status" id="nvidia-status">checking</span>
357
- </div>
358
- </div>
359
- <div class="provider-card" data-provider="google">
360
- <div class="provider-left">
361
- <span class="provider-name">Google AI Studio</span>
362
- <span class="provider-models">gemini-2.0-flash-001, gemini-1.5-pro-001, gemini-1.5-flash-001</span>
363
- </div>
364
- <div class="provider-right">
365
- <span class="provider-status" id="google-status">checking</span>
366
- </div>
367
- </div>
368
- </div>
369
- </section>
370
-
371
- <hr class="divider">
372
-
373
- <!-- Keys -->
374
- <section>
375
- <div class="section-header">
376
- <span class="section-label">Proxy keys</span>
377
- </div>
378
- <div class="key-section" id="key-list">
379
- <div class="key-row">
380
- <span class="key-provider-label">NVIDIA</span>
381
- <span class="key-value masked" id="key-nvidia" data-full="">loading...</span>
382
- <button class="key-toggle" data-key="key-nvidia">Show</button>
383
- </div>
384
- <div class="key-row">
385
- <span class="key-provider-label">GOOGLE</span>
386
- <span class="key-value masked" id="key-google" data-full="">loading...</span>
387
- <button class="key-toggle" data-key="key-google">Show</button>
388
- </div>
389
- </div>
390
- </section>
391
-
392
- <!-- Usage examples -->
393
- <div class="code-section">
394
- <div class="section-header">
395
- <span class="section-label">Usage</span>
396
- </div>
397
- <div class="code-block" id="curl-nvidia">
398
- <button class="code-copy" data-copy="curl-nvidia">Copy</button>
399
- <span class="c"># NVIDIA (deepseek-v4-flash)</span><br>
400
- curl <span class="v">http://localhost:8000/v1/chat/completions</span> \<br>
401
- &nbsp;&nbsp;-H <span class="v">"Content-Type: application/json"</span> \<br>
402
- &nbsp;&nbsp;-H <span class="v">"Authorization: Bearer <span id="curl-nv-key">sk-nvidia-...</span>"</span> \<br>
403
- &nbsp;&nbsp;-d <span class="v">'{"model":"deepseek-ai/deepseek-v4-flash","messages":[{"role":"user","content":"hello"}]}'</span>
404
- </div>
405
- <div class="code-block" id="curl-google">
406
- <button class="code-copy" data-copy="curl-google">Copy</button>
407
- <span class="c"># Google (gemini-2.0-flash)</span><br>
408
- curl <span class="v">http://localhost:8000/v1/chat/completions</span> \<br>
409
- &nbsp;&nbsp;-H <span class="v">"Content-Type: application/json"</span> \<br>
410
- &nbsp;&nbsp;-H <span class="v">"Authorization: Bearer <span id="curl-gc-key">sk-google-...</span>"</span> \<br>
411
- &nbsp;&nbsp;-d <span class="v">'{"model":"gemini-2.0-flash-001","messages":[{"role":"user","content":"hello"}]}'</span>
412
- </div>
413
- </div>
414
-
415
- </div>
416
-
417
- <script>
418
- const HOST = window.location.host;
419
-
420
- function copyText(el, text) {
421
- navigator.clipboard.writeText(text).then(() => {
422
- el.classList.add('copied');
423
- el.textContent = 'Copied';
424
- setTimeout(() => {
425
- el.classList.remove('copied');
426
- el.textContent = 'Copy URL';
427
- }, 1800);
428
- });
429
- }
430
-
431
- document.querySelectorAll('[data-copy]').forEach(btn => {
432
- btn.addEventListener('click', () => {
433
- const id = btn.dataset.copy;
434
- if (id === 'endpoint-url') {
435
- const url = document.getElementById('endpoint-url').textContent;
436
- copyText(btn, url);
437
- } else {
438
- const block = document.getElementById(id);
439
- const text = block.textContent.replace(/^\s+/gm, '') || '';
440
- copyText(btn, text.trim());
441
- }
442
- });
443
- });
444
-
445
- // Key visibility toggle
446
- document.querySelectorAll('.key-toggle').forEach(btn => {
447
- btn.addEventListener('click', () => {
448
- const keyId = btn.dataset.key;
449
- const el = document.getElementById(keyId);
450
- if (el.classList.contains('masked')) {
451
- el.classList.remove('masked');
452
- btn.textContent = 'Hide';
453
- } else {
454
- el.classList.add('masked');
455
- btn.textContent = 'Show';
456
- }
457
- });
458
- });
459
-
460
- // Health check
461
- async function checkHealth() {
462
- const badge = document.getElementById('status-badge');
463
- const text = document.getElementById('status-text');
464
- const dot = badge.querySelector('.dot');
465
-
466
- try {
467
- const res = await fetch('/v1/health');
468
- if (res.ok) {
469
- dot.className = 'dot';
470
- text.textContent = 'running';
471
- return true;
472
- }
473
- } catch {}
474
- dot.className = 'dot offline';
475
- text.textContent = 'unreachable';
476
- return false;
477
- }
478
-
479
- // Key info
480
- async function loadKeys() {
481
- try {
482
- const res = await fetch('/v1/health');
483
- const data = await res.json();
484
- const providers = data.providers || [];
485
-
486
- const nvKey = document.getElementById('key-nvidia');
487
- const gcKey = document.getElementById('key-google');
488
- const curlNvKey = document.getElementById('curl-nv-key');
489
- const curlGcKey = document.getElementById('curl-gc-key');
490
-
491
- // Update status badges
492
- if (providers.includes('nvidia')) {
493
- const s = document.getElementById('nvidia-status');
494
- s.textContent = 'configured';
495
- s.classList.add('configured');
496
- }
497
- if (providers.includes('google')) {
498
- const s = document.getElementById('google-status');
499
- s.textContent = 'configured';
500
- s.classList.add('configured');
501
- }
502
-
503
- // Keys from server
504
- try {
505
- const ks = await fetch('/v1/keys');
506
- if (ks.ok) {
507
- const kd = await ks.json();
508
- if (kd.nvidia) {
509
- const nv = kd.nvidia;
510
- nvKey.textContent = nv;
511
- nvKey.dataset.full = nv;
512
- curlNvKey.textContent = nv;
513
- }
514
- if (kd.google) {
515
- const gc = kd.google;
516
- gcKey.textContent = gc;
517
- gcKey.dataset.full = gc;
518
- curlGcKey.textContent = gc;
519
- }
520
- }
521
- } catch {}
522
- } catch {
523
- document.getElementById('nvidia-status').textContent = 'offline';
524
- document.getElementById('google-status').textContent = 'offline';
525
- }
526
- }
527
-
528
- // Update endpoint URL
529
- document.getElementById('endpoint-url').textContent =
530
- window.location.origin + '/v1/chat/completions';
531
-
532
- checkHealth();
533
- loadKeys();
534
- </script>
535
- </body>
536
- </html>