Elysiadev11 commited on
Commit
491498e
Β·
verified Β·
1 Parent(s): 5f0bc39

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -115
app.py CHANGED
@@ -1,171 +1,129 @@
1
  import os
2
  import time
3
  import random
 
4
  from typing import Dict
5
 
6
  from fastapi import FastAPI, Request, HTTPException
7
- from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
8
-
9
- from ollama import Client
10
 
11
  app = FastAPI()
12
 
13
- # ── CONFIG ─────────────────────────────────────────────
14
  MASTER_API_KEY = os.getenv("MASTER_API_KEY", "changeme")
15
  BASE_URL = os.getenv("BASE_URL", "https://ollama.com")
16
 
17
- # ── LOAD KEYS ──────────────────────────────────────────
18
  def load_keys(prefix="OLLAMA_KEY_"):
19
- pairs = []
20
  for k, v in os.environ.items():
21
  if k.startswith(prefix) and v:
22
- try:
23
- idx = int(k.replace(prefix, ""))
24
- except:
25
- idx = 9999
26
- pairs.append((idx, v.strip()))
27
- pairs.sort(key=lambda x: x[0])
28
- return [v for _, v in pairs]
29
 
30
  API_KEYS = load_keys()
31
- print(f"[INIT] Loaded {len(API_KEYS)} keys")
32
 
33
- # ── STATE ──────────────────────────────────────────────
34
  key_status: Dict[str, Dict] = {
35
- k: {
36
- "status": "active",
37
- "fail": 0,
38
- "cooldown": 0,
39
- "req": 0,
40
- "err": 0,
41
- "last": "-"
42
- }
43
  for k in API_KEYS
44
  }
45
 
46
- # ── AUTH ───────────────────────────────────────────────
47
  def auth(req: Request):
48
  if req.headers.get("Authorization") != f"Bearer {MASTER_API_KEY}":
49
  raise HTTPException(401, "Unauthorized")
50
 
51
- # ── KEY PICKER ─────────────────────────────────────────
52
  def pick_key():
53
  now = time.time()
54
- valid = []
55
-
56
- for k, v in key_status.items():
57
- if v["status"] == "dead":
58
- continue
59
- if v["cooldown"] > now:
60
- continue
61
- valid.append(k)
62
-
63
  return random.choice(valid) if valid else None
64
 
65
- # ── STATUS UPDATE ──────────────────────────────────────
66
  def mark_limit(k):
67
- key_status[k]["status"] = "limited"
68
  key_status[k]["cooldown"] = time.time() + 60
69
- key_status[k]["err"] += 1
70
- key_status[k]["last"] = "rate_limit"
71
 
72
- def mark_dead(k, reason="dead"):
73
  key_status[k]["status"] = "dead"
74
- key_status[k]["err"] += 1
75
- key_status[k]["last"] = reason
76
 
77
  def mark_ok(k):
78
- key_status[k]["status"] = "active"
79
  key_status[k]["fail"] = 0
80
- key_status[k]["last"] = "-"
81
 
82
- # ── STREAM PROXY ───────────────────────────────────────
83
- @app.post("/chat")
84
- async def chat(req: Request):
85
  auth(req)
86
- body = await req.json()
87
 
88
- model = body.get("model")
89
- messages = body.get("messages")
90
 
91
- if not model:
92
- return JSONResponse({"error": "model required"}, status_code=400)
93
 
94
- if not messages:
95
- return JSONResponse({"error": "messages required"}, status_code=400)
 
96
 
97
  for _ in range(len(API_KEYS)):
98
  key = pick_key()
99
-
100
  if not key:
101
- return JSONResponse({"error": "all keys exhausted"}, status_code=503)
102
 
103
- try:
104
- client = Client(
105
- host=BASE_URL,
106
- headers={"Authorization": f"Bearer {key}"}
107
- )
108
-
109
- key_status[key]["req"] += 1
110
 
111
- def stream():
112
- try:
113
- for part in client.chat(model, messages=messages, stream=True):
114
- yield part["message"]["content"]
 
 
 
 
 
 
 
 
 
115
  mark_ok(key)
116
 
117
- except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  key_status[key]["fail"] += 1
119
- key_status[key]["err"] += 1
120
- key_status[key]["last"] = str(e)
121
-
122
- if "429" in str(e):
123
- mark_limit(key)
124
- elif key_status[key]["fail"] >= 3:
125
- mark_dead(key, "fail")
126
-
127
- raise e
128
-
129
- return StreamingResponse(stream(), media_type="text/plain")
130
 
131
  except Exception:
132
- continue
133
-
134
- return JSONResponse({"error": "all keys failed"}, status_code=500)
135
 
136
- # ── DASHBOARD ──────────────────────────────────────────
137
- @app.get("/dashboard")
138
- async def dash(req: Request):
139
- auth(req)
140
-
141
- now = time.time()
142
 
143
- html = """
144
- <html><body style="background:#0f172a;color:white;font-family:sans-serif">
145
- <h2>KEY STATUS</h2>
146
- <table border="1" cellpadding="8">
147
- <tr>
148
- <th>Key</th><th>Status</th><th>Req</th><th>Err</th><th>Cooldown</th><th>Last</th>
149
- </tr>
150
- """
151
-
152
- for k, v in key_status.items():
153
- cd = max(0, int(v["cooldown"] - now))
154
- html += f"""
155
- <tr>
156
- <td>{k[:6]}...</td>
157
- <td>{v['status']}</td>
158
- <td>{v['req']}</td>
159
- <td>{v['err']}</td>
160
- <td>{cd}s</td>
161
- <td>{v['last']}</td>
162
- </tr>
163
- """
164
-
165
- html += "</table></body></html>"
166
- return HTMLResponse(html)
167
-
168
- # ── ROOT ───────────────────────────────────────────────
169
  @app.get("/")
170
- def root():
171
- return {"status": "ok", "keys": len(API_KEYS)}
 
 
 
 
1
  import os
2
  import time
3
  import random
4
+ import httpx
5
  from typing import Dict
6
 
7
  from fastapi import FastAPI, Request, HTTPException
8
+ from fastapi.responses import StreamingResponse, Response
 
 
9
 
10
  app = FastAPI()
11
 
12
+ # ── CONFIG ─────────────────────────────────────
13
  MASTER_API_KEY = os.getenv("MASTER_API_KEY", "changeme")
14
  BASE_URL = os.getenv("BASE_URL", "https://ollama.com")
15
 
16
+ # ── LOAD KEYS ──────────────────────────────────
17
  def load_keys(prefix="OLLAMA_KEY_"):
18
+ keys = []
19
  for k, v in os.environ.items():
20
  if k.startswith(prefix) and v:
21
+ keys.append(v.strip())
22
+ return sorted(keys)
 
 
 
 
 
23
 
24
  API_KEYS = load_keys()
 
25
 
26
+ # ── STATE ──────────────────────────────────────
27
  key_status: Dict[str, Dict] = {
28
+ k: {"fail": 0, "cooldown": 0, "status": "active"}
 
 
 
 
 
 
 
29
  for k in API_KEYS
30
  }
31
 
32
+ # ── AUTH ───────────────────────────────────────
33
  def auth(req: Request):
34
  if req.headers.get("Authorization") != f"Bearer {MASTER_API_KEY}":
35
  raise HTTPException(401, "Unauthorized")
36
 
37
+ # ── KEY PICK ───────────────────────────────────
38
  def pick_key():
39
  now = time.time()
40
+ valid = [
41
+ k for k, v in key_status.items()
42
+ if v["status"] != "dead" and v["cooldown"] < now
43
+ ]
 
 
 
 
 
44
  return random.choice(valid) if valid else None
45
 
46
+ # ── STATUS ─────────────────────────────────────
47
  def mark_limit(k):
 
48
  key_status[k]["cooldown"] = time.time() + 60
 
 
49
 
50
+ def mark_dead(k):
51
  key_status[k]["status"] = "dead"
 
 
52
 
53
  def mark_ok(k):
 
54
  key_status[k]["fail"] = 0
 
55
 
56
+ # ── UNIVERSAL PROXY ────────────────────────────
57
+ @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
58
+ async def proxy(req: Request, path: str):
59
  auth(req)
 
60
 
61
+ # build target url dinamis
62
+ target_url = f"{BASE_URL}/v1/{path}"
63
 
64
+ # ambil body (kalau ada)
65
+ body = await req.body()
66
 
67
+ # copy headers kecuali auth (kita ganti)
68
+ headers = dict(req.headers)
69
+ headers.pop("host", None)
70
 
71
  for _ in range(len(API_KEYS)):
72
  key = pick_key()
 
73
  if not key:
74
+ return Response("All keys exhausted", status_code=503)
75
 
76
+ headers["Authorization"] = f"Bearer {key}"
 
 
 
 
 
 
77
 
78
+ try:
79
+ async with httpx.AsyncClient(timeout=None) as client:
80
+
81
+ r = await client.request(
82
+ method=req.method,
83
+ url=target_url,
84
+ headers=headers,
85
+ content=body,
86
+ params=req.query_params
87
+ )
88
+
89
+ # SUCCESS
90
+ if r.status_code < 400:
91
  mark_ok(key)
92
 
93
+ # STREAMING
94
+ if "text/event-stream" in r.headers.get("content-type", ""):
95
+ return StreamingResponse(
96
+ r.aiter_raw(),
97
+ media_type="text/event-stream"
98
+ )
99
+
100
+ return Response(
101
+ content=r.content,
102
+ status_code=r.status_code,
103
+ headers=dict(r.headers)
104
+ )
105
+
106
+ # RATE LIMIT
107
+ elif r.status_code == 429:
108
+ mark_limit(key)
109
+
110
+ # SERVER ERROR
111
+ elif r.status_code >= 500:
112
  key_status[key]["fail"] += 1
113
+ if key_status[key]["fail"] >= 3:
114
+ mark_dead(key)
 
 
 
 
 
 
 
 
 
115
 
116
  except Exception:
117
+ key_status[key]["fail"] += 1
118
+ if key_status[key]["fail"] >= 3:
119
+ mark_dead(key)
120
 
121
+ return Response("All keys failed", status_code=500)
 
 
 
 
 
122
 
123
+ # ── ROOT ───────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  @app.get("/")
125
+ def home():
126
+ return {
127
+ "status": "universal proxy running",
128
+ "keys": len(API_KEYS)
129
+ }