localhost-llm commited on
Commit
09b9d00
·
verified ·
1 Parent(s): 1907826

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +205 -0
app.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import httpx
4
+ import gradio as gr
5
+ import uvicorn
6
+ from fastapi import FastAPI, Request, Header, HTTPException, File, UploadFile
7
+ from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
8
+ import time
9
+
10
+ # ---------------------------------------------------------------------
11
+ # Configuration
12
+ # ---------------------------------------------------------------------
13
+ BYTEZ_CHAT_URL = "https://api.bytez.com/models/v2/openai/v1/chat/completions "
14
+ BYTEZ_MODELS_URL = "https://api.bytez.com/models/v2/list/models "
15
+ BYTEZ_TTS_URL = "https://api.bytez.com/models/v2/openai/tts-1-hd"
16
+ BYTEZ_STT_URL = "https://api.bytez.com/models/v2/openai/whisper-1"
17
+ BYTEZ_AUTH = os.getenv("BYTEZ_API_KEY")
18
+ LOCAL_API_KEY = os.getenv("LOCAL_API_KEY")
19
+
20
+ # ---------------------------------------------------------------------
21
+ # FastAPI backend
22
+ # ---------------------------------------------------------------------
23
+ api = FastAPI(title="Bytez → OpenAI Proxy")
24
+
25
+ def check_key(auth: str | None):
26
+ if not auth or not auth.startswith("Bearer "):
27
+ raise HTTPException(status_code=401, detail="Missing or invalid API key")
28
+ user_key = auth.split("Bearer ")[1].strip()
29
+ if LOCAL_API_KEY and user_key != LOCAL_API_KEY:
30
+ raise HTTPException(status_code=403, detail="Unauthorized API key")
31
+
32
+ # ---------------------------------------------------------------------
33
+ # Root / health
34
+ # ---------------------------------------------------------------------
35
+ @api.get("/")
36
+ def root():
37
+ return {"status": "ok", "message": "Bytez proxy running"}
38
+
39
+ # ---------------------------------------------------------------------
40
+ # /v1/models → must look OpenAI-style
41
+ # ---------------------------------------------------------------------
42
+ @api.get("/v1/models")
43
+ async def models(authorization: str = Header(None)):
44
+ check_key(authorization)
45
+ if not BYTEZ_AUTH:
46
+ raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
47
+ async with httpx.AsyncClient(timeout=30) as c:
48
+ r = await c.get(BYTEZ_MODELS_URL, headers={"Authorization": BYTEZ_AUTH})
49
+ try:
50
+ data = r.json()
51
+ except json.JSONDecodeError:
52
+ raise HTTPException(status_code=502, detail="Upstream returned invalid JSON")
53
+ # Transform Bytez format → OpenAI format
54
+ models_list = [
55
+ {"id": m.get("id") or m.get("name"), "object": "model"}
56
+ for m in (data if isinstance(data, list) else data.get("data", []))
57
+ ]
58
+ return JSONResponse(
59
+ {"object": "list", "data": models_list},
60
+ headers={"Access-Control-Allow-Origin": "*"}
61
+ )
62
+
63
+ # ---------------------------------------------------------------------
64
+ # /v1/chat/completions
65
+ # ---------------------------------------------------------------------
66
+ @api.post("/v1/chat/completions")
67
+ async def chat(request: Request, authorization: str = Header(None)):
68
+ check_key(authorization)
69
+ if not BYTEZ_AUTH:
70
+ raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
71
+ payload = await request.json()
72
+ stream = payload.get("stream", False)
73
+ headers = {
74
+ "Authorization": BYTEZ_AUTH,
75
+ "Content-Type": "application/json",
76
+ }
77
+ async def event_stream():
78
+ async with httpx.AsyncClient(timeout=120) as client:
79
+ async with client.stream("POST", BYTEZ_CHAT_URL, headers=headers, json=payload) as upstream:
80
+ async for line in upstream.aiter_lines():
81
+ line = line.strip()
82
+ if not line:
83
+ continue
84
+ if line.startswith("data: "):
85
+ json_str = line[6:]
86
+ else:
87
+ json_str = line
88
+ try:
89
+ chunk = json.loads(json_str)
90
+ except json.JSONDecodeError:
91
+ continue
92
+ if json_str == "[DONE]":
93
+ yield "data: [DONE]\n\n"
94
+ break
95
+ content = ""
96
+ if "token" in chunk:
97
+ content = chunk["token"]
98
+ elif "choices" in chunk and len(chunk["choices"]) > 0:
99
+ delta = chunk["choices"][0].get("delta", {})
100
+ content = delta.get("content", "")
101
+ elif "text" in chunk:
102
+ content = chunk["text"]
103
+ else:
104
+ content = str(chunk)
105
+ openai_chunk = {
106
+ "id": "chatcmpl-proxy-stream",
107
+ "object": "chat.completion.chunk",
108
+ "created": int(time.time()),
109
+ "model": payload.get("model", "unknown"),
110
+ "choices": [
111
+ {
112
+ "index": 0,
113
+ "delta": {"role": "assistant", "content": content},
114
+ "finish_reason": None,
115
+ }
116
+ ],
117
+ }
118
+ yield f"data: {json.dumps(openai_chunk)}\n\n"
119
+ yield "data: [DONE]\n\n"
120
+
121
+ if stream:
122
+ return StreamingResponse(
123
+ event_stream(),
124
+ media_type="text/event-stream",
125
+ headers={"Access-Control-Allow-Origin": "*"}
126
+ )
127
+ async with httpx.AsyncClient(timeout=120) as c:
128
+ r = await c.post(BYTEZ_CHAT_URL, headers=headers, json=payload)
129
+ try:
130
+ data = r.json()
131
+ except json.JSONDecodeError:
132
+ raise HTTPException(status_code=502, detail="Upstream returned invalid JSON")
133
+ if "choices" not in data:
134
+ content = (
135
+ data.get("output")
136
+ or data.get("response")
137
+ or data.get("message")
138
+ or str(data)
139
+ )
140
+ data = {
141
+ "id": "chatcmpl-proxy",
142
+ "object": "chat.completion",
143
+ "choices": [
144
+ {"index": 0, "message": {"role": "assistant", "content": content}}
145
+ ],
146
+ }
147
+ return JSONResponse(data, headers={"Access-Control-Allow-Origin": "*"})
148
+
149
+ # ---------------------------------------------------------------------
150
+ # /v1/tts (Text-to-Speech)
151
+ # ---------------------------------------------------------------------
152
+ @api.post("/v1/tts")
153
+ async def tts(request: Request, authorization: str = Header(None)):
154
+ check_key(authorization)
155
+ if not BYTEZ_AUTH:
156
+ raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
157
+ payload = await request.json()
158
+ text = payload.get("text", "")
159
+ if not text:
160
+ raise HTTPException(status_code=400, detail="Text not provided for TTS")
161
+ headers = {
162
+ "Authorization": BYTEZ_AUTH,
163
+ "Content-Type": "application/json",
164
+ }
165
+ async with httpx.AsyncClient(timeout=120) as c:
166
+ r = await c.post(BYTEZ_TTS_URL, headers=headers, json={"text": text})
167
+ if r.status_code != 200:
168
+ raise HTTPException(status_code=r.status_code, detail="TTS request failed")
169
+ audio_data = r.content
170
+ return StreamingResponse(io.BytesIO(audio_data), media_type="audio/wav")
171
+
172
+ # ---------------------------------------------------------------------
173
+ # /v1/stt (Speech-to-Text)
174
+ # ---------------------------------------------------------------------
175
+ @api.post("/v1/stt")
176
+ async def stt(file: UploadFile = File(...), authorization: str = Header(None)):
177
+ check_key(authorization)
178
+ if not BYTEZ_AUTH:
179
+ raise HTTPException(status_code=500, detail="Server BYTEZ_API_KEY not configured")
180
+ headers = {"Authorization": BYTEZ_AUTH}
181
+ file_content = await file.read()
182
+ files = {"file": (file.filename, file_content, file.content_type)}
183
+
184
+ async with httpx.AsyncClient(timeout=120) as c:
185
+ r = await c.post(BYTEZ_STT_URL, headers=headers, files=files)
186
+
187
+ try:
188
+ data = r.json()
189
+ except json.JSONDecodeError:
190
+ raise HTTPException(status_code=502, detail="Upstream returned invalid JSON")
191
+
192
+ transcript = data.get("transcription", "")
193
+ return {"transcription": transcript}
194
+
195
+ # ---------------------------------------------------------------------
196
+ # Minimal Gradio UI (to make HF Space start)
197
+ # ---------------------------------------------------------------------
198
+ with gr.Blocks() as ui:
199
+ gr.Markdown("### ✅ Jwero Bytez → OpenAI Proxy\n"
200
+ "Endpoints: `/v1/models`, `/v1/chat/completions`, `/v1/tts`, `/v1/stt`")
201
+
202
+ demo = gr.mount_gradio_app(api, ui, path="/")
203
+
204
+ if __name__ == "__main__":
205
+ uvicorn.run(demo, host="0.0.0.0", port=7860)