File size: 3,289 Bytes
a056d23
 
 
 
 
139fc91
a056d23
 
139fc91
a056d23
 
 
 
 
eeb5b0e
a056d23
 
 
 
 
 
 
 
 
 
 
 
 
 
eeb5b0e
a056d23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eeb5b0e
a056d23
 
 
 
 
 
 
 
 
 
 
 
8fc20f8
 
 
 
 
 
 
 
 
a056d23
 
 
 
 
 
 
 
 
 
 
 
 
eeb5b0e
a056d23
 
 
 
eeb5b0e
a056d23
 
 
 
 
 
 
 
 
 
 
 
 
61a39ca
 
eeb5b0e
61a39ca
a056d23
 
 
 
 
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
import json
import logging
from typing import Any

from fastapi import APIRouter, Depends, FastAPI, Request
from fastapi.responses import JSONResponse, Response
from starlette.exceptions import HTTPException as StarletteHTTPException

from config import HOST, PORT, PROVIDER_KEYS
from proxy.auth import get_real_key, resolve_provider
from proxy.client import forward_request
from proxy.providers import PROVIDERS

logger = logging.getLogger(__name__)
api = APIRouter(prefix="/v1")



MODELS = {
    "nvidia": [
        "deepseek-ai/deepseek-v4-pro",
        "deepseek-ai/deepseek-v4-flash",
        "moonshotai/kimi-k2.6",
        "moonshotai/kimi-k2.5",
        "google/gemma-4-31b-it",
    ],
}


@api.get("/models")
async def list_models() -> JSONResponse:
    data = {
        "object": "list",
        "data": [
            {
                "id": model,
                "object": "model",
                "owned_by": provider,
            }
            for provider, models in MODELS.items()
            for model in models
        ],
    }
    return JSONResponse(content=data)


@api.post("/chat/completions")
async def chat_completions(request: Request) -> Response:
    provider = resolve_provider(request)
    real_key = get_real_key(provider)

    try:
        body = await request.json()
    except Exception as exc:
        raise StarletteHTTPException(status_code=400, detail=f"Invalid JSON body: {exc}") from exc

    if not isinstance(body, dict):
        raise StarletteHTTPException(status_code=400, detail="Request body must be a JSON object")

    model = body.get("model")
    allowed = MODELS.get(provider.name, [])
    if model and model not in allowed:
        raise StarletteHTTPException(
            status_code=400,
            detail=f"Model '{model}' is not available for provider '{provider.name}'. Available: {', '.join(allowed)}",
        )

    logger.info("Proxying request to provider=%s model=%s", provider.name, model or "default")

    result = await forward_request(provider, real_key, request, body)

    if isinstance(result, Response):
        return result

    return Response(
        content=result["content"],
        status_code=result["status_code"],
        headers={k: v for k, v in result["headers"].items() if k.lower() not in ("content-encoding", "transfer-encoding", "content-length")},
    )


@api.get("/health")
async def health() -> JSONResponse:
    return JSONResponse({"status": "ok", "providers": list(PROVIDERS.keys())})


@api.get("/keys")
async def proxy_keys() -> JSONResponse:
    masked = {}
    for name, key in PROVIDER_KEYS.items():
        if key:
            visible = key[:8] + "..." if len(key) > 11 else key
            masked[name] = visible
    return JSONResponse(masked)


def create_app() -> FastAPI:
    app = FastAPI(title="OpenAI-Compatible API Proxy", version="1.0.0")
    app.include_router(api)

    @app.get("/")
    async def root():
        return {"message": "OpenAI API Proxy", "docs": "/v1/health"}

    @app.exception_handler(StarletteHTTPException)
    async def http_exception_handler(_request: Request, exc: StarletteHTTPException) -> JSONResponse:
        return JSONResponse(status_code=exc.status_code, content={"error": {"message": exc.detail, "type": "proxy_error"}})

    return app