Elysiadev11 commited on
Commit
8f7852e
Β·
verified Β·
1 Parent(s): 491498e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -45
app.py CHANGED
@@ -1,83 +1,106 @@
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,
@@ -86,44 +109,64 @@ async def proxy(req: Request, path: str):
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
  }
 
1
  import os
2
  import time
3
  import random
4
+ import json
5
  import httpx
6
  from typing import Dict
7
 
8
  from fastapi import FastAPI, Request, HTTPException
9
+ from fastapi.responses import Response, StreamingResponse, JSONResponse
10
 
11
  app = FastAPI()
12
 
13
+ # ─────────────────────────────────────────────
14
+ # CONFIG
15
+ # ─────────────────────────────────────────────
16
  MASTER_API_KEY = os.getenv("MASTER_API_KEY", "changeme")
17
  BASE_URL = os.getenv("BASE_URL", "https://ollama.com")
18
 
19
+ # ─────────────────────────────────────────────
20
+ # LOAD KEYS (OLLAMA_KEY_1, OLLAMA_KEY_2, ...)
21
+ # ─────────────────────────────────────────────
22
  def load_keys(prefix="OLLAMA_KEY_"):
23
  keys = []
24
  for k, v in os.environ.items():
25
  if k.startswith(prefix) and v:
26
  keys.append(v.strip())
27
+ return keys
28
 
29
  API_KEYS = load_keys()
30
+ print(f"[INIT] Loaded {len(API_KEYS)} keys")
31
 
32
+ # ─────────────────────────────────────────────
33
+ # KEY STATE
34
+ # ─────────────────────────────────────────────
35
  key_status: Dict[str, Dict] = {
36
+ k: {
37
+ "fail": 0,
38
+ "cooldown": 0,
39
+ "status": "active"
40
+ }
41
  for k in API_KEYS
42
  }
43
 
44
+ # ─────────────────────────────────────────────
45
+ # AUTH
46
+ # ─────────────────────────────────────────────
47
  def auth(req: Request):
48
  if req.headers.get("Authorization") != f"Bearer {MASTER_API_KEY}":
49
  raise HTTPException(401, "Unauthorized")
50
 
51
+ # ─────────────────────────────────────────────
52
+ # PICK KEY
53
+ # ─────────────────────────────────────────────
54
  def pick_key():
55
  now = time.time()
56
+
57
  valid = [
58
  k for k, v in key_status.items()
59
  if v["status"] != "dead" and v["cooldown"] < now
60
  ]
61
+
62
  return random.choice(valid) if valid else None
63
 
64
+ # ─────────────────────────────────────────────
65
+ # STATUS HANDLER
66
+ # ─────────────────────────────────────────────
67
  def mark_limit(k):
68
  key_status[k]["cooldown"] = time.time() + 60
69
+ key_status[k]["status"] = "limited"
70
 
71
  def mark_dead(k):
72
  key_status[k]["status"] = "dead"
73
 
74
  def mark_ok(k):
75
  key_status[k]["fail"] = 0
76
+ key_status[k]["status"] = "active"
77
 
78
+ # ─────────────────────────────────────────────
79
+ # UNIVERSAL OPENAI PROXY (/v1/*)
80
+ # ─────────────────────────────────────────────
81
  @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
82
  async def proxy(req: Request, path: str):
83
  auth(req)
84
 
 
85
  target_url = f"{BASE_URL}/v1/{path}"
86
 
 
87
  body = await req.body()
88
 
89
+ # clean headers
90
  headers = dict(req.headers)
91
  headers.pop("host", None)
92
+ headers.pop("content-length", None)
93
 
94
  for _ in range(len(API_KEYS)):
95
  key = pick_key()
96
+
97
  if not key:
98
+ return JSONResponse({"error": "all keys exhausted"}, 503)
99
 
100
  headers["Authorization"] = f"Bearer {key}"
101
 
102
  try:
103
  async with httpx.AsyncClient(timeout=None) as client:
 
104
  r = await client.request(
105
  method=req.method,
106
  url=target_url,
 
109
  params=req.query_params
110
  )
111
 
112
+ # ─────────────────────────────
113
+ # SUCCESS
114
+ # ─────────────────────────────
115
+ if r.status_code < 400:
116
+ mark_ok(key)
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ content_type = r.headers.get("content-type", "")
 
 
119
 
120
+ # streaming (SSE)
121
+ if "text/event-stream" in content_type:
122
+ return StreamingResponse(
123
+ r.aiter_raw(),
124
+ media_type="text/event-stream"
125
+ )
126
+
127
+ return Response(
128
+ content=r.content,
129
+ status_code=r.status_code,
130
+ headers=dict(r.headers)
131
+ )
132
 
133
+ # ─────────────────────────────
134
+ # RATE LIMIT
135
+ # ─────────────────────────────
136
+ if r.status_code == 429:
137
+ mark_limit(key)
138
+
139
+ # ─────────────────────────────
140
+ # SERVER ERROR
141
+ # ─────────────────────────────
142
+ if r.status_code >= 500:
143
+ key_status[key]["fail"] += 1
144
+ if key_status[key]["fail"] >= 3:
145
+ mark_dead(key)
146
+
147
+ except Exception as e:
148
  key_status[key]["fail"] += 1
149
+
150
  if key_status[key]["fail"] >= 3:
151
  mark_dead(key)
152
 
153
+ print(f"[ERROR] key={key[:6]} err={str(e)}")
154
+
155
+ return JSONResponse(
156
+ {
157
+ "error": "all keys failed",
158
+ "hint": "check API keys or BASE_URL"
159
+ },
160
+ 500
161
+ )
162
 
163
+ # ─────────────────────────────────────────────
164
+ # HEALTH CHECK
165
+ # ─────────────────────────────────────────────
166
  @app.get("/")
167
+ def root():
168
  return {
169
+ "status": "ok",
170
+ "keys": len(API_KEYS),
171
+ "mode": "openai-compatible-proxy"
172
  }