Elysiadev11 commited on
Commit
6f02e11
Β·
verified Β·
1 Parent(s): 8f7852e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -67
app.py CHANGED
@@ -10,15 +10,15 @@ 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():
@@ -29,41 +29,35 @@ def load_keys(prefix="OLLAMA_KEY_"):
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"
@@ -75,25 +69,22 @@ 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
 
@@ -101,72 +92,61 @@ async def proxy(req: Request, path: str):
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,
107
  headers=headers,
108
  content=body,
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
  }
 
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
21
+ # ─────────────────────────────
22
  def load_keys(prefix="OLLAMA_KEY_"):
23
  keys = []
24
  for k, v in os.environ.items():
 
29
  API_KEYS = load_keys()
30
  print(f"[INIT] Loaded {len(API_KEYS)} keys")
31
 
32
+ # ─────────────────────────────
33
+ # STATE
34
+ # ─────────────────────────────
35
  key_status: Dict[str, Dict] = {
36
+ k: {"fail": 0, "cooldown": 0, "status": "active"}
 
 
 
 
37
  for k in API_KEYS
38
  }
39
 
40
+ # ─────────────────────────────
41
  # AUTH
42
+ # ─────────────────────────────
43
  def auth(req: Request):
44
  if req.headers.get("Authorization") != f"Bearer {MASTER_API_KEY}":
45
  raise HTTPException(401, "Unauthorized")
46
 
47
+ # ─────────────────────────────
48
+ # KEY PICKER
49
+ # ─────────────────────────────
50
  def pick_key():
51
  now = time.time()
 
52
  valid = [
53
  k for k, v in key_status.items()
54
  if v["status"] != "dead" and v["cooldown"] < now
55
  ]
 
56
  return random.choice(valid) if valid else None
57
 
58
+ # ─────────────────────────────
59
  # STATUS HANDLER
60
+ # ─────────────────────────────
61
  def mark_limit(k):
62
  key_status[k]["cooldown"] = time.time() + 60
63
  key_status[k]["status"] = "limited"
 
69
  key_status[k]["fail"] = 0
70
  key_status[k]["status"] = "active"
71
 
72
+ # ─────────────────────────────
73
+ # UNIVERSAL OPENAI PROXY
74
+ # ─────────────────────────────
75
  @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
76
  async def proxy(req: Request, path: str):
77
  auth(req)
78
 
79
  target_url = f"{BASE_URL}/v1/{path}"
 
80
  body = await req.body()
81
 
 
82
  headers = dict(req.headers)
83
  headers.pop("host", None)
84
  headers.pop("content-length", None)
85
 
86
  for _ in range(len(API_KEYS)):
87
  key = pick_key()
 
88
  if not key:
89
  return JSONResponse({"error": "all keys exhausted"}, 503)
90
 
 
92
 
93
  try:
94
  async with httpx.AsyncClient(timeout=None) as client:
95
+
96
+ # πŸ”₯ STREAM SAFE MODE (FIX UTAMA)
97
+ async with client.stream(
98
  method=req.method,
99
  url=target_url,
100
  headers=headers,
101
  content=body,
102
  params=req.query_params
103
+ ) as r:
104
+
105
+ # ERROR RESPONSE
106
+ if r.status_code >= 400:
107
+ err = await r.aread()
108
+ if r.status_code == 429:
109
+ mark_limit(key)
110
+ elif r.status_code >= 500:
111
+ key_status[key]["fail"] += 1
112
+ if key_status[key]["fail"] >= 3:
113
+ mark_dead(key)
114
 
115
+ continue
 
 
 
 
116
 
117
+ mark_ok(key)
118
 
119
+ # STREAMING RESPONSE
 
120
  return StreamingResponse(
121
+ r.aiter_bytes(),
122
+ media_type=r.headers.get(
123
+ "content-type",
124
+ "application/json"
125
+ )
126
  )
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  except Exception as e:
129
  key_status[key]["fail"] += 1
 
130
  if key_status[key]["fail"] >= 3:
131
  mark_dead(key)
132
 
133
+ print(f"[ERROR] key={key[:6]} err={e}")
134
 
135
  return JSONResponse(
136
  {
137
  "error": "all keys failed",
138
+ "hint": "check keys or model access"
139
  },
140
  500
141
  )
142
 
143
+ # ─────────────────────────────
144
  # HEALTH CHECK
145
+ # ─────────────────────────────
146
  @app.get("/")
147
  def root():
148
  return {
149
  "status": "ok",
150
+ "mode": "openai-grade-proxy",
151
+ "keys": len(API_KEYS)
152
  }