imbecome commited on
Commit
c6579d0
·
verified ·
1 Parent(s): a2823bc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -316
app.py CHANGED
@@ -8,6 +8,8 @@ import shutil
8
  import tempfile
9
  import uuid
10
  import time
 
 
11
 
12
  from fastapi import FastAPI, Request, UploadFile, File
13
  from fastapi.responses import JSONResponse, StreamingResponse
@@ -17,54 +19,54 @@ from faster_whisper import WhisperModel
17
  from gtts import gTTS
18
 
19
  # ====================================
20
- # CONFIG
21
  # ====================================
22
-
23
  TIMEOUT = 120
24
-
25
- OPENROUTER_KEYS = os.getenv("OPENROUTER_KEYS", "").split(",")
26
-
27
- CF_ACCOUNTS = []
28
- cf_env = os.getenv("CLOUDFLARE_ACCOUNTS", "")
29
-
30
- for acc in cf_env.split(","):
31
- if ":" in acc:
32
- acc_id, token = acc.split(":")
33
- CF_ACCOUNTS.append({"id": acc_id, "token": token})
34
-
35
- OLLAMA_URL = os.getenv("OLLAMA_URL", "http://127.0.0.1:11434/api/chat")
36
-
37
- # ====================================
38
- # MODEL ROUTER
39
- # ====================================
40
-
41
  MODEL_ROUTER = {
42
-
43
  "deepseek": "deepseek/deepseek-r1",
44
-
45
  "gpt": "openai/gpt-4o-mini",
46
-
47
  "claude": "anthropic/claude-3-haiku",
48
-
49
  "gemini": "google/gemini-2.0-flash"
50
-
51
  }
52
 
53
- # ====================================
54
- # WHISPER
55
- # ====================================
56
-
57
  whisper_model = WhisperModel(
58
- "tiny",
59
- device="cpu",
60
- compute_type="int8"
 
61
  )
62
 
63
  # ====================================
64
- # FASTAPI
65
  # ====================================
 
66
 
67
- app = FastAPI(title="AI Gateway")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  app.add_middleware(
70
  CORSMiddleware,
@@ -74,360 +76,143 @@ app.add_middleware(
74
  allow_headers=["*"],
75
  )
76
 
77
- # ====================================
78
- # CACHE
79
- # ====================================
80
-
81
  CACHE = {}
82
 
83
- def cache_key(messages):
84
- return hashlib.md5(json.dumps(messages).encode()).hexdigest()
85
-
86
  def get_cache(messages):
87
- key = cache_key(messages)
88
  return CACHE.get(key)
89
 
90
  def set_cache(messages, data):
91
- key = cache_key(messages)
92
  CACHE[key] = data
93
 
94
- # ====================================
95
- # PROVIDERS
96
- # ====================================
97
-
98
  async def call_ollama(messages, model):
99
-
100
  try:
101
-
102
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
103
-
104
  res = await client.post(
105
- OLLAMA_URL,
106
- json={
107
- "model": model,
108
- "messages": messages,
109
- "stream": False
110
- }
111
  )
112
-
113
- if res.status_code == 200:
114
-
115
- data = res.json()
116
-
117
- return data["message"]["content"]
118
-
119
- except:
120
- return None
121
-
122
 
123
  async def call_openrouter(messages, model):
124
-
125
- random.shuffle(OPENROUTER_KEYS)
126
-
127
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
128
-
129
- for key in OPENROUTER_KEYS:
130
-
131
  try:
132
-
133
  res = await client.post(
134
  "https://openrouter.ai/api/v1/chat/completions",
135
- headers={
136
- "Authorization": f"Bearer {key}",
137
- "Content-Type": "application/json"
138
- },
139
- json={
140
- "model": model,
141
- "messages": messages
142
- }
143
  )
144
-
145
- if res.status_code == 200:
146
-
147
- data = res.json()
148
-
149
- return data["choices"][0]["message"]["content"]
150
-
151
- except:
152
- continue
153
-
154
  return None
155
 
156
-
157
  async def call_cloudflare(messages):
158
-
159
- random.shuffle(CF_ACCOUNTS)
160
-
161
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
162
-
163
- for acc in CF_ACCOUNTS:
164
-
165
  try:
166
-
167
  url = f"https://api.cloudflare.com/client/v4/accounts/{acc['id']}/ai/run/@cf/meta/llama-3-8b-instruct"
168
-
169
- res = await client.post(
170
- url,
171
- headers={
172
- "Authorization": f"Bearer {acc['token']}"
173
- },
174
- json={
175
- "messages": messages
176
- }
177
- )
178
-
179
- if res.status_code == 200:
180
-
181
- data = res.json()
182
-
183
- return data["result"]["response"]
184
-
185
- except:
186
- continue
187
-
188
  return None
189
 
190
-
191
- # ====================================
192
- # PARALLEL ROUTER
193
- # ====================================
194
-
195
  async def parallel_ai(messages, model):
196
-
197
  tasks = [
198
-
199
  asyncio.create_task(call_ollama(messages, model)),
200
-
201
  asyncio.create_task(call_openrouter(messages, model)),
202
-
203
  asyncio.create_task(call_cloudflare(messages))
204
-
205
  ]
206
-
207
- done, pending = await asyncio.wait(
208
- tasks,
209
- return_when=asyncio.FIRST_COMPLETED
210
- )
211
-
212
- for p in pending:
213
- p.cancel()
214
-
215
  for d in done:
216
-
217
- result = d.result()
218
-
219
- if result:
220
- return result
221
-
222
  return None
223
 
224
-
225
- # ====================================
226
- # QUEUE
227
- # ====================================
228
-
229
- QUEUE = asyncio.Queue()
230
-
231
- async def worker():
232
-
233
- while True:
234
-
235
- messages, model, future = await QUEUE.get()
236
-
237
- try:
238
-
239
- result = await parallel_ai(messages, model)
240
-
241
- future.set_result(result)
242
-
243
- except Exception as e:
244
-
245
- future.set_exception(e)
246
-
247
- QUEUE.task_done()
248
-
249
-
250
- @app.on_event("startup")
251
- async def startup():
252
-
253
- for _ in range(4):
254
- asyncio.create_task(worker())
255
-
256
-
257
  # ====================================
258
- # STREAM GENERATOR
259
  # ====================================
260
 
261
- async def stream_text(text):
262
-
263
- words = text.split(" ")
264
-
265
- for w in words:
266
-
267
- chunk = {
268
- "choices": [
269
- {
270
- "delta": {
271
- "content": w + " "
272
- }
273
- }
274
- ]
275
- }
276
-
277
- yield f"data: {json.dumps(chunk)}\n\n"
278
-
279
- await asyncio.sleep(0.02)
280
-
281
- yield "data: [DONE]\n\n"
282
-
283
-
284
- # ====================================
285
- # CHAT COMPLETION
286
- # ====================================
287
 
288
  @app.post("/v1/chat/completions")
289
  async def chat(request: Request):
290
-
291
  body = await request.json()
292
-
293
  messages = body.get("messages", [])
294
- model = body.get("model", "deepseek")
295
-
296
  stream = body.get("stream", False)
297
-
298
- model = MODEL_ROUTER.get(model, model)
299
-
300
- cached = get_cache(messages)
301
-
302
- if cached and not stream:
303
- return cached
304
-
305
- loop = asyncio.get_event_loop()
306
- future = loop.create_future()
307
-
308
  await QUEUE.put((messages, model, future))
309
-
310
  result = await future
311
 
312
  if not result:
313
- return JSONResponse({"error": "AI failed"}, status_code=500)
314
 
315
  if stream:
316
-
317
- return StreamingResponse(
318
- stream_text(result),
319
- media_type="text/event-stream"
320
- )
 
 
321
 
322
  response = {
323
  "id": str(uuid.uuid4()),
324
  "object": "chat.completion",
325
  "created": int(time.time()),
326
  "model": model,
327
- "choices": [
328
- {
329
- "message": {
330
- "role": "assistant",
331
- "content": result
332
- }
333
- }
334
- ]
335
  }
336
-
337
  set_cache(messages, response)
338
-
339
  return response
340
 
341
-
342
- # ====================================
343
- # WHISPER STT
344
- # ====================================
345
-
346
  @app.post("/v1/audio/transcriptions")
347
  async def transcribe(file: UploadFile = File(...)):
348
-
349
- with tempfile.NamedTemporaryFile(delete=False) as tmp:
350
-
351
  shutil.copyfileobj(file.file, tmp)
352
- path = tmp.name
353
-
354
- segments, info = whisper_model.transcribe(path)
355
-
356
- text = ""
357
-
358
- for s in segments:
359
- text += s.text
360
-
361
- os.remove(path)
362
-
363
- return {
364
- "text": text,
365
- "language": info.language
366
- }
367
-
368
-
369
- # ====================================
370
- # TEXT TO SPEECH
371
- # ====================================
372
 
373
  @app.post("/v1/audio/speech")
374
  async def tts(request: Request):
375
-
376
  body = await request.json()
377
-
378
- text = body.get("input", "")
379
-
380
- voice = body.get("voice", "en")
381
-
382
- lang = "en"
383
-
384
- if voice == "id":
385
- lang = "id"
386
-
387
- tts = gTTS(text=text, lang=lang)
388
-
389
  tmp_path = f"/tmp/{uuid.uuid4()}.mp3"
 
390
 
391
- tts.save(tmp_path)
392
-
393
- def stream():
394
-
395
- with open(tmp_path, "rb") as f:
396
  yield from f
 
397
 
398
- os.remove(tmp_path)
399
-
400
- return StreamingResponse(stream(), media_type="audio/mpeg")
401
-
402
-
403
- # ====================================
404
- # MODELS
405
- # ====================================
406
-
407
- @app.get("/v1/models")
408
- async def models():
409
-
410
- return {
411
- "data": [
412
-
413
- {"id": "deepseek"},
414
- {"id": "gpt"},
415
- {"id": "claude"},
416
- {"id": "gemini"},
417
- {"id": "whisper-1"},
418
- {"id": "tts-1"}
419
-
420
- ]
421
- }
422
-
423
-
424
- # ====================================
425
- # HEALTH
426
- # ====================================
427
-
428
- @app.get("/")
429
- async def root():
430
 
431
- return {
432
- "status": "AI Gateway Running"
433
- }
 
8
  import tempfile
9
  import uuid
10
  import time
11
+ import uvicorn
12
+ from contextlib import asynccontextmanager
13
 
14
  from fastapi import FastAPI, Request, UploadFile, File
15
  from fastapi.responses import JSONResponse, StreamingResponse
 
19
  from gtts import gTTS
20
 
21
  # ====================================
22
+ # CONFIG & MODELS
23
  # ====================================
 
24
  TIMEOUT = 120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  MODEL_ROUTER = {
 
26
  "deepseek": "deepseek/deepseek-r1",
 
27
  "gpt": "openai/gpt-4o-mini",
 
28
  "claude": "anthropic/claude-3-haiku",
 
29
  "gemini": "google/gemini-2.0-flash"
 
30
  }
31
 
32
+ # Inisialisasi Whisper (Pastikan path cache sesuai dengan Docker)
 
 
 
33
  whisper_model = WhisperModel(
34
+ "tiny",
35
+ device="cpu",
36
+ compute_type="int8",
37
+ download_root="./models_cache"
38
  )
39
 
40
  # ====================================
41
+ # LIFESPAN & WORKER
42
  # ====================================
43
+ QUEUE = asyncio.Queue()
44
 
45
+ async def worker():
46
+ while True:
47
+ messages, model, future = await QUEUE.get()
48
+ try:
49
+ result = await parallel_ai(messages, model)
50
+ future.set_result(result)
51
+ except Exception as e:
52
+ future.set_exception(e)
53
+ finally:
54
+ QUEUE.task_done()
55
+
56
+ @asynccontextmanager
57
+ async def lifespan(app: FastAPI):
58
+ # Startup: Jalankan 4 worker
59
+ workers = [asyncio.create_task(worker()) for _ in range(4)]
60
+ print("=== WORKERS READY ===")
61
+ yield
62
+ # Shutdown: Batalkan worker jika perlu
63
+ for w in workers:
64
+ w.cancel()
65
+
66
+ # ====================================
67
+ # FASTAPI SETUP
68
+ # ====================================
69
+ app = FastAPI(title="AI Gateway", lifespan=lifespan)
70
 
71
  app.add_middleware(
72
  CORSMiddleware,
 
76
  allow_headers=["*"],
77
  )
78
 
 
 
 
 
79
  CACHE = {}
80
 
81
+ # --- Helper Functions (Cache & AI Calls) ---
 
 
82
  def get_cache(messages):
83
+ key = hashlib.md5(json.dumps(messages).encode()).hexdigest()
84
  return CACHE.get(key)
85
 
86
  def set_cache(messages, data):
87
+ key = hashlib.md5(json.dumps(messages).encode()).hexdigest()
88
  CACHE[key] = data
89
 
 
 
 
 
90
  async def call_ollama(messages, model):
 
91
  try:
 
92
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
 
93
  res = await client.post(
94
+ os.getenv("OLLAMA_URL", "http://127.0.0.1:11434/api/chat"),
95
+ json={"model": model, "messages": messages, "stream": False}
 
 
 
 
96
  )
97
+ return res.json()["message"]["content"] if res.status_code == 200 else None
98
+ except: return None
 
 
 
 
 
 
 
 
99
 
100
  async def call_openrouter(messages, model):
101
+ keys = os.getenv("OPENROUTER_KEYS", "").split(",")
102
+ random.shuffle(keys)
 
103
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
104
+ for key in keys:
 
 
105
  try:
 
106
  res = await client.post(
107
  "https://openrouter.ai/api/v1/chat/completions",
108
+ headers={"Authorization": f"Bearer {key}"},
109
+ json={"model": model, "messages": messages}
 
 
 
 
 
 
110
  )
111
+ if res.status_code == 200: return res.json()["choices"][0]["message"]["content"]
112
+ except: continue
 
 
 
 
 
 
 
 
113
  return None
114
 
 
115
  async def call_cloudflare(messages):
116
+ cf_env = os.getenv("CLOUDFLARE_ACCOUNTS", "")
117
+ accounts = [{"id": a.split(":")[0], "token": a.split(":")[1]} for a in cf_env.split(",") if ":" in a]
118
+ random.shuffle(accounts)
119
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
120
+ for acc in accounts:
 
 
121
  try:
 
122
  url = f"https://api.cloudflare.com/client/v4/accounts/{acc['id']}/ai/run/@cf/meta/llama-3-8b-instruct"
123
+ res = await client.post(url, headers={"Authorization": f"Bearer {acc['token']}"}, json={"messages": messages})
124
+ if res.status_code == 200: return res.json()["result"]["response"]
125
+ except: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  return None
127
 
 
 
 
 
 
128
  async def parallel_ai(messages, model):
 
129
  tasks = [
 
130
  asyncio.create_task(call_ollama(messages, model)),
 
131
  asyncio.create_task(call_openrouter(messages, model)),
 
132
  asyncio.create_task(call_cloudflare(messages))
 
133
  ]
134
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
135
+ for p in pending: p.cancel()
 
 
 
 
 
 
 
136
  for d in done:
137
+ res = d.result()
138
+ if res: return res
 
 
 
 
139
  return None
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ====================================
142
+ # ENDPOINTS
143
  # ====================================
144
 
145
+ @app.get("/")
146
+ async def root():
147
+ return {"status": "AI Gateway Running", "engine": "FastAPI Lifespan"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  @app.post("/v1/chat/completions")
150
  async def chat(request: Request):
 
151
  body = await request.json()
 
152
  messages = body.get("messages", [])
153
+ model_key = body.get("model", "deepseek")
 
154
  stream = body.get("stream", False)
155
+
156
+ model = MODEL_ROUTER.get(model_key, model_key)
157
+
158
+ if not stream:
159
+ cached = get_cache(messages)
160
+ if cached: return cached
161
+
162
+ future = asyncio.get_event_loop().create_future()
 
 
 
163
  await QUEUE.put((messages, model, future))
 
164
  result = await future
165
 
166
  if not result:
167
+ return JSONResponse({"error": "AI failed to respond"}, status_code=500)
168
 
169
  if stream:
170
+ async def stream_gen():
171
+ for word in result.split(" "):
172
+ chunk = {"choices": [{"delta": {"content": word + " "}}]}
173
+ yield f"data: {json.dumps(chunk)}\n\n"
174
+ await asyncio.sleep(0.01)
175
+ yield "data: [DONE]\n\n"
176
+ return StreamingResponse(stream_gen(), media_type="text/event-stream")
177
 
178
  response = {
179
  "id": str(uuid.uuid4()),
180
  "object": "chat.completion",
181
  "created": int(time.time()),
182
  "model": model,
183
+ "choices": [{"message": {"role": "assistant", "content": result}}]
 
 
 
 
 
 
 
184
  }
 
185
  set_cache(messages, response)
 
186
  return response
187
 
 
 
 
 
 
188
  @app.post("/v1/audio/transcriptions")
189
  async def transcribe(file: UploadFile = File(...)):
190
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".tmp") as tmp:
 
 
191
  shutil.copyfileobj(file.file, tmp)
192
+ tmp_path = tmp.name
193
+
194
+ try:
195
+ segments, info = whisper_model.transcribe(tmp_path)
196
+ text = "".join([s.text for s in segments])
197
+ return {"text": text, "language": info.language}
198
+ finally:
199
+ if os.path.exists(tmp_path): os.remove(tmp_path)
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  @app.post("/v1/audio/speech")
202
  async def tts(request: Request):
 
203
  body = await request.json()
204
+ text, voice = body.get("input", ""), body.get("voice", "en")
205
+ lang = "id" if voice == "id" else "en"
206
+
 
 
 
 
 
 
 
 
 
207
  tmp_path = f"/tmp/{uuid.uuid4()}.mp3"
208
+ gTTS(text=text, lang=lang).save(tmp_path)
209
 
210
+ def iterfile():
211
+ with open(tmp_path, mode="rb") as f:
 
 
 
212
  yield from f
213
+ if os.path.exists(tmp_path): os.remove(tmp_path)
214
 
215
+ return StreamingResponse(iterfile(), media_type="audio/mpeg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ if __name__ == "__main__":
218
+ uvicorn.run(app, host="0.0.0.0", port=7860)