"configs/datasets/CHARM/charm_rea_gen_f8fca2.py" did not exist on "07a6dacf33141fdd176c5870574cbba5b73c27e3"
main.py 12 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
2
from fastapi import FastAPI, Request, Response, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
Timothy J. Baek's avatar
Timothy J. Baek committed
3
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
Timothy J. Baek's avatar
Timothy J. Baek committed
4
5

import requests
Timothy J. Baek's avatar
Timothy J. Baek committed
6
7
import aiohttp
import asyncio
Timothy J. Baek's avatar
Timothy J. Baek committed
8
import json
9
import logging
Timothy J. Baek's avatar
Timothy J. Baek committed
10

Timothy J. Baek's avatar
Timothy J. Baek committed
11
12
from pydantic import BaseModel

Timothy J. Baek's avatar
Timothy J. Baek committed
13

Timothy J. Baek's avatar
Timothy J. Baek committed
14
15
from apps.web.models.users import Users
from constants import ERROR_MESSAGES
Timothy J. Baek's avatar
Timothy J. Baek committed
16
17
18
19
20
21
from utils.utils import (
    decode_token,
    get_current_user,
    get_verified_user,
    get_admin_user,
)
22
from config import (
23
    SRC_LOG_LEVELS,
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
24
    ENABLE_OPENAI_API,
25
26
27
    OPENAI_API_BASE_URLS,
    OPENAI_API_KEYS,
    CACHE_DIR,
Timothy J. Baek's avatar
Timothy J. Baek committed
28
    ENABLE_MODEL_FILTER,
29
    MODEL_FILTER_LIST,
30
    MODEL_CONFIG,
31
    AppConfig,
32
)
Timothy J. Baek's avatar
Timothy J. Baek committed
33
34
from typing import List, Optional

Timothy J. Baek's avatar
Timothy J. Baek committed
35
36
37

import hashlib
from pathlib import Path
Timothy J. Baek's avatar
Timothy J. Baek committed
38

39
40
41
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OPENAI"])

Timothy J. Baek's avatar
Timothy J. Baek committed
42
43
44
45
46
47
48
49
50
app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Timothy J. Baek's avatar
Timothy J. Baek committed
51

52
53
app.state.config = AppConfig()

Timothy J. Baek's avatar
Timothy J. Baek committed
54
55
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
56
app.state.MODEL_CONFIG = MODEL_CONFIG.value.get("openai", [])
Timothy J. Baek's avatar
Timothy J. Baek committed
57

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
58
59

app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
60
61
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
Timothy J. Baek's avatar
Timothy J. Baek committed
62
63
64

app.state.MODELS = {}

Timothy J. Baek's avatar
Timothy J. Baek committed
65

Timothy J. Baek's avatar
Timothy J. Baek committed
66
67
68
69
70
71
@app.middleware("http")
async def check_url(request: Request, call_next):
    if len(app.state.MODELS) == 0:
        await get_all_models()
    else:
        pass
Timothy J. Baek's avatar
Timothy J. Baek committed
72

Timothy J. Baek's avatar
Timothy J. Baek committed
73
74
    response = await call_next(request)
    return response
Timothy J. Baek's avatar
Timothy J. Baek committed
75
76


Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@app.get("/config")
async def get_config(user=Depends(get_admin_user)):
    return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}


class OpenAIConfigForm(BaseModel):
    enable_openai_api: Optional[bool] = None


@app.post("/config/update")
async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
    app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
    return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}


Timothy J. Baek's avatar
Timothy J. Baek committed
92
93
class UrlsUpdateForm(BaseModel):
    urls: List[str]
Timothy J. Baek's avatar
Timothy J. Baek committed
94
95


Timothy J. Baek's avatar
Timothy J. Baek committed
96
97
class KeysUpdateForm(BaseModel):
    keys: List[str]
Timothy J. Baek's avatar
Timothy J. Baek committed
98
99


Timothy J. Baek's avatar
Timothy J. Baek committed
100
101
@app.get("/urls")
async def get_openai_urls(user=Depends(get_admin_user)):
102
    return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
103

Timothy J. Baek's avatar
Timothy J. Baek committed
104

Timothy J. Baek's avatar
Timothy J. Baek committed
105
106
@app.post("/urls/update")
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
107
    await get_all_models()
108
109
    app.state.config.OPENAI_API_BASE_URLS = form_data.urls
    return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
Timothy J. Baek's avatar
Timothy J. Baek committed
110
111


Timothy J. Baek's avatar
Timothy J. Baek committed
112
113
@app.get("/keys")
async def get_openai_keys(user=Depends(get_admin_user)):
114
    return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
Timothy J. Baek's avatar
Timothy J. Baek committed
115
116
117
118


@app.post("/keys/update")
async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
119
120
    app.state.config.OPENAI_API_KEYS = form_data.keys
    return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
Timothy J. Baek's avatar
Timothy J. Baek committed
121
122


Timothy J. Baek's avatar
Timothy J. Baek committed
123
@app.post("/audio/speech")
124
async def speech(request: Request, user=Depends(get_verified_user)):
Timothy J. Baek's avatar
Timothy J. Baek committed
125
126
    idx = None
    try:
127
        idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
Timothy J. Baek's avatar
Timothy J. Baek committed
128
129
130
131
132
133
134
135
136
137
138
139
140
        body = await request.body()
        name = hashlib.sha256(body).hexdigest()

        SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
        SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
        file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
        file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")

        # Check if the file already exists in the cache
        if file_path.is_file():
            return FileResponse(file_path)

        headers = {}
141
        headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
Timothy J. Baek's avatar
Timothy J. Baek committed
142
        headers["Content-Type"] = "application/json"
143
144
145
        if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
            headers["HTTP-Referer"] = "https://openwebui.com/"
            headers["X-Title"] = "Open WebUI"
Timothy J. Baek's avatar
Timothy J. Baek committed
146
        r = None
Timothy J. Baek's avatar
Timothy J. Baek committed
147
148
        try:
            r = requests.post(
149
                url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
Timothy J. Baek's avatar
Timothy J. Baek committed
150
151
152
153
                data=body,
                headers=headers,
                stream=True,
            )
Timothy J. Baek's avatar
Timothy J. Baek committed
154

Timothy J. Baek's avatar
Timothy J. Baek committed
155
            r.raise_for_status()
Timothy J. Baek's avatar
Timothy J. Baek committed
156

Timothy J. Baek's avatar
Timothy J. Baek committed
157
158
159
160
            # Save the streaming content to a file
            with open(file_path, "wb") as f:
                for chunk in r.iter_content(chunk_size=8192):
                    f.write(chunk)
Timothy J. Baek's avatar
Timothy J. Baek committed
161

Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
            with open(file_body_path, "w") as f:
                json.dump(json.loads(body.decode("utf-8")), f)
Timothy J. Baek's avatar
Timothy J. Baek committed
164

Timothy J. Baek's avatar
Timothy J. Baek committed
165
166
            # Return the saved file
            return FileResponse(file_path)
Timothy J. Baek's avatar
Timothy J. Baek committed
167

Timothy J. Baek's avatar
Timothy J. Baek committed
168
        except Exception as e:
169
            log.exception(e)
Timothy J. Baek's avatar
Timothy J. Baek committed
170
171
172
173
174
175
176
177
178
            error_detail = "Open WebUI: Server Connection Error"
            if r is not None:
                try:
                    res = r.json()
                    if "error" in res:
                        error_detail = f"External: {res['error']}"
                except:
                    error_detail = f"External: {e}"

Timothy J. Baek's avatar
Timothy J. Baek committed
179
180
181
            raise HTTPException(
                status_code=r.status_code if r else 500, detail=error_detail
            )
Timothy J. Baek's avatar
Timothy J. Baek committed
182
183
184

    except ValueError:
        raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
Timothy J. Baek's avatar
Timothy J. Baek committed
185
186


Timothy J. Baek's avatar
Timothy J. Baek committed
187
async def fetch_url(url, key):
Timothy J. Baek's avatar
Timothy J. Baek committed
188
    timeout = aiohttp.ClientTimeout(total=5)
Timothy J. Baek's avatar
Timothy J. Baek committed
189
    try:
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
190
191
        if key != "":
            headers = {"Authorization": f"Bearer {key}"}
Timothy J. Baek's avatar
Timothy J. Baek committed
192
            async with aiohttp.ClientSession(timeout=timeout) as session:
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
193
194
195
196
                async with session.get(url, headers=headers) as response:
                    return await response.json()
        else:
            return None
Timothy J. Baek's avatar
Timothy J. Baek committed
197
198
    except Exception as e:
        # Handle connection error here
199
        log.error(f"Connection error: {e}")
Timothy J. Baek's avatar
Timothy J. Baek committed
200
201
202
203
        return None


def merge_models_lists(model_lists):
Timothy J. Baek's avatar
Timothy J. Baek committed
204
    log.info(f"merge_models_lists {model_lists}")
Timothy J. Baek's avatar
Timothy J. Baek committed
205
206
207
    merged_list = []

    for idx, models in enumerate(model_lists):
Timothy J. Baek's avatar
Timothy J. Baek committed
208
209
210
211
212
        if models is not None and "error" not in models:
            merged_list.extend(
                [
                    {**model, "urlIdx": idx}
                    for model in models
213
                    if "api.openai.com"
214
                    not in app.state.config.OPENAI_API_BASE_URLS[idx]
Timothy J. Baek's avatar
Timothy J. Baek committed
215
216
217
                    or "gpt" in model["id"]
                ]
            )
Timothy J. Baek's avatar
Timothy J. Baek committed
218

Timothy J. Baek's avatar
Timothy J. Baek committed
219
    return merged_list
Timothy J. Baek's avatar
Timothy J. Baek committed
220
221


Timothy J. Baek's avatar
Timothy J. Baek committed
222
async def get_all_models():
223
    log.info("get_all_models()")
224

225
    if (
226
227
        len(app.state.config.OPENAI_API_KEYS) == 1
        and app.state.config.OPENAI_API_KEYS[0] == ""
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
228
    ) or not app.state.config.ENABLE_OPENAI_API:
229
230
231
        models = {"data": []}
    else:
        tasks = [
232
233
            fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
            for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
234
        ]
Timothy J. Baek's avatar
Timothy J. Baek committed
235

236
        responses = await asyncio.gather(*tasks)
Timothy J. Baek's avatar
Timothy J. Baek committed
237
238
        log.info(f"get_all_models:responses() {responses}")

239
240
        models = {
            "data": merge_models_lists(
Timothy J. Baek's avatar
Timothy J. Baek committed
241
242
                list(
                    map(
Timothy J. Baek's avatar
Timothy J. Baek committed
243
                        lambda response: (
Timothy J. Baek's avatar
Timothy J. Baek committed
244
                            response["data"]
Timothy J. Baek's avatar
Timothy J. Baek committed
245
246
                            if (response and "data" in response)
                            else (response if isinstance(response, list) else None)
Timothy J. Baek's avatar
Timothy J. Baek committed
247
                        ),
Timothy J. Baek's avatar
Timothy J. Baek committed
248
249
250
                        responses,
                    )
                )
251
252
            )
        }
Timothy J. Baek's avatar
Timothy J. Baek committed
253

254
255
256
        for model in models["data"]:
            add_custom_info_to_model(model)

257
        log.info(f"models: {models}")
258
        app.state.MODELS = {model["id"]: model for model in models["data"]}
Timothy J. Baek's avatar
Timothy J. Baek committed
259

260
261
262
263
264
    return models


def add_custom_info_to_model(model: dict):
    model["custom_info"] = next(
265
        (item for item in app.state.MODEL_CONFIG if item["id"] == model["id"]), {}
266
    )
Timothy J. Baek's avatar
Timothy J. Baek committed
267

Timothy J. Baek's avatar
Timothy J. Baek committed
268
269
270

@app.get("/models")
@app.get("/models/{url_idx}")
Timothy J. Baek's avatar
Timothy J. Baek committed
271
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
Timothy J. Baek's avatar
Timothy J. Baek committed
272
    if url_idx == None:
Timothy J. Baek's avatar
Timothy J. Baek committed
273
        models = await get_all_models()
Timothy J. Baek's avatar
Timothy J. Baek committed
274
        if app.state.config.ENABLE_MODEL_FILTER:
Timothy J. Baek's avatar
Timothy J. Baek committed
275
            if user.role == "user":
276
277
                models["data"] = list(
                    filter(
Timothy J. Baek's avatar
Timothy J. Baek committed
278
                        lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
279
280
                        models["data"],
                    )
Timothy J. Baek's avatar
Timothy J. Baek committed
281
282
283
                )
                return models
        return models
Timothy J. Baek's avatar
Timothy J. Baek committed
284
    else:
285
        url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
Timothy J. Baek's avatar
Timothy J. Baek committed
286
287
288

        r = None

Timothy J. Baek's avatar
Timothy J. Baek committed
289
290
291
292
293
294
295
296
297
298
299
300
        try:
            r = requests.request(method="GET", url=f"{url}/models")
            r.raise_for_status()

            response_data = r.json()
            if "api.openai.com" in url:
                response_data["data"] = list(
                    filter(lambda model: "gpt" in model["id"], response_data["data"])
                )

            return response_data
        except Exception as e:
301
            log.exception(e)
Timothy J. Baek's avatar
Timothy J. Baek committed
302
303
304
305
306
307
308
309
310
311
312
313
314
            error_detail = "Open WebUI: Server Connection Error"
            if r is not None:
                try:
                    res = r.json()
                    if "error" in res:
                        error_detail = f"External: {res['error']}"
                except:
                    error_detail = f"External: {e}"

            raise HTTPException(
                status_code=r.status_code if r else 500,
                detail=error_detail,
            )
Timothy J. Baek's avatar
Timothy J. Baek committed
315
316


Timothy J. Baek's avatar
Timothy J. Baek committed
317
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
318
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
Timothy J. Baek's avatar
Timothy J. Baek committed
319
    idx = 0
Timothy J. Baek's avatar
Timothy J. Baek committed
320

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
321
322
    body = await request.body()
    # TODO: Remove below after gpt-4-vision fix from Open AI
323
324
    # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
    try:
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
325
326
327
        body = body.decode("utf-8")
        body = json.loads(body)

Timothy J. Baek's avatar
Timothy J. Baek committed
328
329
        idx = app.state.MODELS[body.get("model")]["urlIdx"]

330
        # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
331
        # This is a workaround until OpenAI fixes the issue with this model
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
332
        if body.get("model") == "gpt-4-vision-preview":
333
334
            if "max_tokens" not in body:
                body["max_tokens"] = 4000
335
            log.debug("Modified body_dict:", body)
336

Sakkus's avatar
Sakkus committed
337
        # Fix for ChatGPT calls failing because the num_ctx key is in body
338
        if "num_ctx" in body:
Sakkus's avatar
Sakkus committed
339
340
341
            # If 'num_ctx' is in the dictionary, delete it
            # Leaving it there generates an error with the
            # OpenAI API (Feb 2024)
342
            del body["num_ctx"]
Sakkus's avatar
Sakkus committed
343

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
344
345
346
        # Convert the modified body back to JSON
        body = json.dumps(body)
    except json.JSONDecodeError as e:
347
        log.error("Error loading request body into a dictionary:", e)
Timothy J. Baek's avatar
Timothy J. Baek committed
348

349
350
    url = app.state.config.OPENAI_API_BASE_URLS[idx]
    key = app.state.config.OPENAI_API_KEYS[idx]
Timothy J. Baek's avatar
Timothy J. Baek committed
351
352
353
354
355
356

    target_url = f"{url}/{path}"

    if key == "":
        raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)

357
    headers = {}
Timothy J. Baek's avatar
Timothy J. Baek committed
358
    headers["Authorization"] = f"Bearer {key}"
Timothy J. Baek's avatar
Timothy J. Baek committed
359
    headers["Content-Type"] = "application/json"
Timothy J. Baek's avatar
Timothy J. Baek committed
360

Timothy J. Baek's avatar
Timothy J. Baek committed
361
362
    r = None

Timothy J. Baek's avatar
Timothy J. Baek committed
363
364
365
366
    try:
        r = requests.request(
            method=request.method,
            url=target_url,
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
367
            data=body,
Timothy J. Baek's avatar
Timothy J. Baek committed
368
369
370
371
372
373
            headers=headers,
            stream=True,
        )

        r.raise_for_status()

374
375
376
377
378
379
380
381
382
383
        # Check if response is SSE
        if "text/event-stream" in r.headers.get("Content-Type", ""):
            return StreamingResponse(
                r.iter_content(chunk_size=8192),
                status_code=r.status_code,
                headers=dict(r.headers),
            )
        else:
            response_data = r.json()
            return response_data
Timothy J. Baek's avatar
Timothy J. Baek committed
384
    except Exception as e:
385
        log.exception(e)
Timothy J. Baek's avatar
Timothy J. Baek committed
386
        error_detail = "Open WebUI: Server Connection Error"
Timothy J. Baek's avatar
Timothy J. Baek committed
387
388
389
390
        if r is not None:
            try:
                res = r.json()
                if "error" in res:
Timothy J. Baek's avatar
Timothy J. Baek committed
391
                    error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
Timothy J. Baek's avatar
Timothy J. Baek committed
392
393
394
            except:
                error_detail = f"External: {e}"

Timothy J. Baek's avatar
Timothy J. Baek committed
395
396
397
        raise HTTPException(
            status_code=r.status_code if r else 500, detail=error_detail
        )