File size: 4,576 Bytes
f0103c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8566f99
f0103c9
8566f99
 
f0103c9
 
 
 
 
 
 
 
8566f99
f0103c9
8566f99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0103c9
 
 
 
 
 
 
 
 
 
 
8566f99
f0103c9
 
8566f99
f0103c9
 
8566f99
f0103c9
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import os
import time
import httpx
import json
import re
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI(title="KAI STUDIO ROUTER")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["*"]
)

BACKENDS = {
    "qwen": {
        "url": os.environ.get("KAI_QWEN_URL", "https://your-qwen-space.hf.space"),
        "key": os.environ.get("KEY_QWEN"),
        "models": ["Qwen2.5-7B-Instruct"]
    },
    "mistral": {
        "url": os.environ.get("KAI_MISTRAL_URL", "https://your-mistral-space.hf.space"),
        "key": os.environ.get("KEY_MISTRAL"),
        "models": ["Mistral-7B-v0.3"]
    }
}

class Message(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    messages: List[Message]
    max_tokens: Optional[int] = 400
    temperature: Optional[float] = 0.7
    stream: bool = True
    model: Optional[str] = None

CODE_KEYWORDS = re.compile(r'\b(def|class|import|function|const|let|var|public|private|return|python|javascript|java|c\+\+|sql|html|css|json|api|docker|git|kod|funkcja|program|skrypt|algorytm)\b', re.I)
MATH_KEYWORDS = re.compile(r'\b(oblicz|policz|r贸wnanie|ca艂ka|pochodna|macierz|prawdopodobie艅stwo|logarytm|pierwiastek|ile to|=\?|\d+\s*[\+\-\*\/]\s*\d+)\b', re.I)
CREATIVE_KEYWORDS = re.compile(r'\b(napisz|wiersz|opowiadanie|historia|bajka|rap|tekst|piosenka|kreatywny|stw贸rz|wymy艣l|zr贸b post|dialog)\b', re.I)

def route_model(messages: List[Message], forced_model: Optional[str] = None) -> str:
    if forced_model and forced_model in BACKENDS:
        return forced_model

    user_text = " ".join([m.content for m in messages if m.role == "user"]).lower()

    if CODE_KEYWORDS.search(user_text) or MATH_KEYWORDS.search(user_text):
        return "qwen"

    if CREATIVE_KEYWORDS.search(user_text):
        return "mistral"

    pl_chars = len(re.findall(r'[膮膰臋艂艅贸艣藕偶]', user_text))
    if pl_chars > 2:
        return "mistral"

    return "qwen"

# --- 2 oddzielne funkcje: stream i non-stream ---
async def proxy_stream(backend_name: str, request_data: dict):
    backend = BACKENDS[backend_name]
    url = f"{backend['url']}/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {backend['key']}",
        "Content-Type": "application/json"
    }

    async with httpx.AsyncClient(timeout=180.0) as client:
        async with client.stream("POST", url, json=request_data, headers=headers) as r:
            if r.status_code!= 200:
                err = await r.aread()
                raise HTTPException(r.status_code, f"Backend {backend_name} error: {err.decode()}")

            yield f": x-kai-model-used {backend_name}\n\n"
            yield f": connected\n\n"

            async for chunk in r.aiter_bytes():
                yield chunk

async def proxy_non_stream(backend_name: str, request_data: dict):
    backend = BACKENDS[backend_name]
    url = f"{backend['url']}/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {backend['key']}",
        "Content-Type": "application/json"
    }

    async with httpx.AsyncClient(timeout=180.0) as client:
        r = await client.post(url, json=request_data, headers=headers)
        if r.status_code!= 200:
            raise HTTPException(r.status_code, f"Backend {backend_name} error: {r.text}")
        data = r.json()
        data["kai_model_used"] = backend_name
        return data

@app.get("/", response_class=HTMLResponse)
async def serve_frontend():
    with open("static/index.html", "r", encoding="utf-8") as f:
        return HTMLResponse(content=f.read())

@app.post("/v1/chat/completions")
async def chat_completions(data: ChatRequest, request: Request):
    chosen_backend = route_model(data.messages, data.model)

    request_data = data.dict()
    request_data.pop("model", None)

    if data.stream:
        generator = proxy_stream(chosen_backend, request_data)
        return StreamingResponse(generator, media_type="text/event-stream")
    else:
        result = await proxy_non_stream(chosen_backend, request_data)
        return JSONResponse(content=result)

@app.get("/health")
async def health():
    return {"status": "ok", "backends": list(BACKENDS.keys())}

app.mount("/static", StaticFiles(directory="static"), name="static")