Spaces:
Running
Running
Commit ·
ea89131
1
Parent(s): d866e5b
Fix actual models
Browse files- README.md +32 -3
- app/factory.py +8 -20
- app/main.py +40 -29
- app/models.py +10 -6
- app/providers/hf_openai.py +57 -45
README.md
CHANGED
|
@@ -1,12 +1,41 @@
|
|
| 1 |
---
|
| 2 |
title: Llm Proxy
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
short_description:
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Llm Proxy
|
| 3 |
+
emoji: "🤗"
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
short_description: OpenAI-compatible proxy for Hugging Face Inference Providers
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# LLM proxy
|
| 13 |
+
|
| 14 |
+
The service exposes an OpenAI-like `/v1/chat/completions` endpoint and routes calls through Hugging Face Inference Providers.
|
| 15 |
+
|
| 16 |
+
## Configuration
|
| 17 |
+
|
| 18 |
+
- `HF_TOKEN`: Hugging Face user token with the **Inference Providers** permission. `HF_API_KEY` remains supported for compatibility.
|
| 19 |
+
- `API_KEYS`: comma-separated bearer tokens accepted by this proxy.
|
| 20 |
+
- `HF_MODELS`: optional comma-separated `alias=model-id` mappings. Default: `gpt-oss=openai/gpt-oss-120b:cerebras`.
|
| 21 |
+
- `HF_TIMEOUT_SECONDS`: optional provider timeout, default `60`.
|
| 22 |
+
|
| 23 |
+
Do not append a provider suffix unless you intentionally want to pin one. With a plain model ID, the Hugging Face router can select an available provider.
|
| 24 |
+
|
| 25 |
+
Example `.env`:
|
| 26 |
+
|
| 27 |
+
```dotenv
|
| 28 |
+
HF_TOKEN=hf_your_token
|
| 29 |
+
API_KEYS=local-secret
|
| 30 |
+
HF_MODELS=gpt-oss=openai/gpt-oss-120b:cerebras
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
Run locally with `start.bat`, then call:
|
| 34 |
+
|
| 35 |
+
```powershell
|
| 36 |
+
$headers = @{ Authorization = "Bearer local-secret" }
|
| 37 |
+
$body = @{ model = "gpt-oss"; messages = @(@{ role = "user"; content = "Привет!" }) } | ConvertTo-Json -Depth 4
|
| 38 |
+
Invoke-RestMethod http://127.0.0.1:8000/v1/chat/completions -Method Post -Headers $headers -ContentType "application/json" -Body $body
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
Free Hugging Face accounts receive only a small monthly inference credit. Requests stop after the credit is exhausted unless additional credits are purchased.
|
app/factory.py
CHANGED
|
@@ -1,25 +1,13 @@
|
|
| 1 |
-
from .providers.hf_openai import HFOpenAIProvider
|
|
|
|
| 2 |
|
| 3 |
class ProviderFactory:
|
| 4 |
-
|
| 5 |
-
"arch-router": HFOpenAIProvider,
|
| 6 |
-
"phi-3-mini": HFOpenAIProvider,
|
| 7 |
-
"gemma-2b": HFOpenAIProvider,
|
| 8 |
-
"mistral-7b": HFOpenAIProvider,
|
| 9 |
-
"llama-3b": HFOpenAIProvider,
|
| 10 |
-
"qwen-3b": HFOpenAIProvider,
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
_instances = {}
|
| 14 |
|
| 15 |
@classmethod
|
| 16 |
-
def get_provider(cls, model_name: str):
|
| 17 |
-
if model_name not in
|
| 18 |
raise ValueError(f"Unsupported model: {model_name}")
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
if cache_key not in cls._instances:
|
| 23 |
-
cls._instances[cache_key] = provider_class()
|
| 24 |
-
|
| 25 |
-
return cls._instances[cache_key]
|
|
|
|
| 1 |
+
from .providers.hf_openai import HFOpenAIProvider, configured_models
|
| 2 |
+
|
| 3 |
|
| 4 |
class ProviderFactory:
|
| 5 |
+
_instance = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
@classmethod
|
| 8 |
+
def get_provider(cls, model_name: str) -> HFOpenAIProvider:
|
| 9 |
+
if model_name not in configured_models():
|
| 10 |
raise ValueError(f"Unsupported model: {model_name}")
|
| 11 |
+
if cls._instance is None:
|
| 12 |
+
cls._instance = HFOpenAIProvider()
|
| 13 |
+
return cls._instance
|
|
|
|
|
|
|
|
|
|
|
|
app/main.py
CHANGED
|
@@ -1,55 +1,66 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
|
| 4 |
from dotenv import load_dotenv
|
|
|
|
| 5 |
|
| 6 |
load_dotenv()
|
| 7 |
|
| 8 |
from .auth import verify_api_key
|
| 9 |
from .factory import ProviderFactory
|
| 10 |
from .models import ChatRequest, ChatResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
app = FastAPI(title="LLM API Proxy", version="1.0.0")
|
| 13 |
|
| 14 |
@app.get("/")
|
| 15 |
async def root():
|
| 16 |
-
return {"message": "LLM API Proxy is running", "version": "
|
|
|
|
| 17 |
|
| 18 |
@app.get("/v1/models")
|
| 19 |
-
async def list_models(
|
| 20 |
-
"""Возвращает список доступных моделей"""
|
| 21 |
return {
|
| 22 |
-
"
|
| 23 |
-
|
| 24 |
-
{"id":
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
{"id": "llama-3b", "name": "Llama 3.2 3B (HF)", "provider": "huggingface"},
|
| 28 |
-
{"id": "qwen-3b", "name": "Qwen 2.5 3B (HF)", "provider": "huggingface"},
|
| 29 |
-
]
|
| 30 |
}
|
| 31 |
|
|
|
|
| 32 |
@app.post("/v1/chat/completions")
|
| 33 |
-
async def chat_completion(
|
| 34 |
-
request: ChatRequest,
|
| 35 |
-
api_key: str = Depends(verify_api_key)
|
| 36 |
-
):
|
| 37 |
try:
|
| 38 |
provider = ProviderFactory.get_provider(request.model)
|
| 39 |
result = await provider.generate(
|
| 40 |
messages=[{"role": m.role, "content": m.content} for m in request.messages],
|
| 41 |
max_tokens=request.max_tokens,
|
| 42 |
temperature=request.temperature,
|
| 43 |
-
model=request.model
|
| 44 |
)
|
| 45 |
-
|
| 46 |
return ChatResponse(
|
| 47 |
-
id=f"
|
| 48 |
-
choices=[
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
)
|
| 52 |
-
except ValueError as
|
| 53 |
-
raise HTTPException(status_code=400, detail=str(
|
| 54 |
-
except Exception as
|
| 55 |
-
raise HTTPException(status_code=502, detail=f"
|
|
|
|
| 1 |
+
from contextlib import asynccontextmanager
|
| 2 |
+
from uuid import uuid4
|
| 3 |
+
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
+
from fastapi import Depends, FastAPI, HTTPException
|
| 6 |
|
| 7 |
load_dotenv()
|
| 8 |
|
| 9 |
from .auth import verify_api_key
|
| 10 |
from .factory import ProviderFactory
|
| 11 |
from .models import ChatRequest, ChatResponse
|
| 12 |
+
from .providers.hf_openai import configured_models
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@asynccontextmanager
|
| 16 |
+
async def lifespan(_: FastAPI):
|
| 17 |
+
yield
|
| 18 |
+
if ProviderFactory._instance is not None:
|
| 19 |
+
await ProviderFactory._instance.client.close()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
app = FastAPI(title="LLM API Proxy", version="2.0.0", lifespan=lifespan)
|
| 23 |
|
|
|
|
| 24 |
|
| 25 |
@app.get("/")
|
| 26 |
async def root():
|
| 27 |
+
return {"message": "LLM API Proxy is running", "version": "2.0.0"}
|
| 28 |
+
|
| 29 |
|
| 30 |
@app.get("/v1/models")
|
| 31 |
+
async def list_models(_: str = Depends(verify_api_key)):
|
|
|
|
| 32 |
return {
|
| 33 |
+
"object": "list",
|
| 34 |
+
"data": [
|
| 35 |
+
{"id": alias, "object": "model", "owned_by": "huggingface", "hf_model": model_id}
|
| 36 |
+
for alias, model_id in configured_models().items()
|
| 37 |
+
],
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
+
|
| 41 |
@app.post("/v1/chat/completions")
|
| 42 |
+
async def chat_completion(request: ChatRequest, _: str = Depends(verify_api_key)):
|
|
|
|
|
|
|
|
|
|
| 43 |
try:
|
| 44 |
provider = ProviderFactory.get_provider(request.model)
|
| 45 |
result = await provider.generate(
|
| 46 |
messages=[{"role": m.role, "content": m.content} for m in request.messages],
|
| 47 |
max_tokens=request.max_tokens,
|
| 48 |
temperature=request.temperature,
|
| 49 |
+
model=request.model,
|
| 50 |
)
|
|
|
|
| 51 |
return ChatResponse(
|
| 52 |
+
id=f"chatcmpl-{uuid4().hex}",
|
| 53 |
+
choices=[
|
| 54 |
+
{
|
| 55 |
+
"index": 0,
|
| 56 |
+
"message": {"role": "assistant", "content": result["content"]},
|
| 57 |
+
"finish_reason": "stop",
|
| 58 |
+
}
|
| 59 |
+
],
|
| 60 |
+
usage={"total_tokens": result["total_tokens"]},
|
| 61 |
+
model=request.model,
|
| 62 |
)
|
| 63 |
+
except ValueError as exc:
|
| 64 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 65 |
+
except Exception as exc:
|
| 66 |
+
raise HTTPException(status_code=502, detail=f"Hugging Face error: {exc}") from exc
|
app/models.py
CHANGED
|
@@ -1,18 +1,22 @@
|
|
| 1 |
-
from
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
|
| 4 |
class Message(BaseModel):
|
| 5 |
role: str
|
| 6 |
content: str
|
| 7 |
|
|
|
|
| 8 |
class ChatRequest(BaseModel):
|
| 9 |
-
model: str
|
| 10 |
messages: List[Message]
|
| 11 |
-
max_tokens:
|
| 12 |
-
temperature:
|
|
|
|
| 13 |
|
| 14 |
class ChatResponse(BaseModel):
|
| 15 |
id: str
|
| 16 |
choices: List[dict]
|
| 17 |
usage: dict
|
| 18 |
-
model: str
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
|
| 6 |
class Message(BaseModel):
|
| 7 |
role: str
|
| 8 |
content: str
|
| 9 |
|
| 10 |
+
|
| 11 |
class ChatRequest(BaseModel):
|
| 12 |
+
model: str = "gpt-oss"
|
| 13 |
messages: List[Message]
|
| 14 |
+
max_tokens: int = Field(default=256, ge=1, le=4096)
|
| 15 |
+
temperature: float = Field(default=0.7, ge=0, le=2)
|
| 16 |
+
|
| 17 |
|
| 18 |
class ChatResponse(BaseModel):
|
| 19 |
id: str
|
| 20 |
choices: List[dict]
|
| 21 |
usage: dict
|
| 22 |
+
model: str
|
app/providers/hf_openai.py
CHANGED
|
@@ -1,52 +1,64 @@
|
|
| 1 |
import os
|
| 2 |
-
from
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
from .base import BaseLLMProvider
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
class HFOpenAIProvider(BaseLLMProvider):
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
"
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
"qwen-3b": "Qwen/Qwen2.5-3B-Instruct:hf-inference",
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
def __init__(self):
|
| 19 |
-
self.api_key = os.getenv("HF_API_KEY")
|
| 20 |
-
if not self.api_key:
|
| 21 |
-
raise ValueError("HF_API_KEY not set")
|
| 22 |
-
|
| 23 |
-
self.client = OpenAI(
|
| 24 |
base_url="https://router.huggingface.co/v1",
|
| 25 |
-
api_key=
|
|
|
|
|
|
|
| 26 |
)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
return "katanemo/Arch-Router-1.5B:hf-inference"
|
| 33 |
-
|
| 34 |
-
async def generate(self, messages: List[Dict[str, str]], **kwargs):
|
| 35 |
-
model = self._get_model_id(kwargs.get("model", "arch-router"))
|
| 36 |
-
|
| 37 |
try:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
from typing import Any, Dict, List
|
| 3 |
+
|
| 4 |
+
from openai import AsyncOpenAI
|
| 5 |
+
|
| 6 |
from .base import BaseLLMProvider
|
| 7 |
|
| 8 |
+
|
| 9 |
+
DEFAULT_MODEL = "openai/gpt-oss-120b:cerebras"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def configured_models() -> Dict[str, str]:
|
| 13 |
+
"""Read aliases from HF_MODELS: alias=model,alias2=model2."""
|
| 14 |
+
raw = os.getenv("HF_MODELS", f"gpt-oss={DEFAULT_MODEL}")
|
| 15 |
+
models: Dict[str, str] = {}
|
| 16 |
+
for item in raw.split(","):
|
| 17 |
+
if not item.strip():
|
| 18 |
+
continue
|
| 19 |
+
alias, separator, model_id = item.partition("=")
|
| 20 |
+
alias = alias.strip()
|
| 21 |
+
model_id = model_id.strip() if separator else alias
|
| 22 |
+
if alias and model_id:
|
| 23 |
+
models[alias] = model_id
|
| 24 |
+
if not models:
|
| 25 |
+
raise ValueError("HF_MODELS does not contain any valid model definitions")
|
| 26 |
+
return models
|
| 27 |
+
|
| 28 |
+
|
| 29 |
class HFOpenAIProvider(BaseLLMProvider):
|
| 30 |
+
"""Hugging Face Inference Providers through its OpenAI-compatible router."""
|
| 31 |
+
|
| 32 |
+
def __init__(self) -> None:
|
| 33 |
+
token = os.getenv("HF_TOKEN") or os.getenv("HF_API_KEY")
|
| 34 |
+
if not token:
|
| 35 |
+
raise ValueError("HF_TOKEN (or legacy HF_API_KEY) is not set")
|
| 36 |
+
self.models = configured_models()
|
| 37 |
+
self.client = AsyncOpenAI(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
base_url="https://router.huggingface.co/v1",
|
| 39 |
+
api_key=token,
|
| 40 |
+
timeout=float(os.getenv("HF_TIMEOUT_SECONDS", "60")),
|
| 41 |
+
max_retries=2,
|
| 42 |
)
|
| 43 |
+
|
| 44 |
+
async def generate(
|
| 45 |
+
self, messages: List[Dict[str, str]], **kwargs: Any
|
| 46 |
+
) -> Dict[str, Any]:
|
| 47 |
+
alias = kwargs.get("model", next(iter(self.models)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
+
model_id = self.models[alias]
|
| 50 |
+
except KeyError as exc:
|
| 51 |
+
raise ValueError(f"Unsupported model: {alias}") from exc
|
| 52 |
+
|
| 53 |
+
response = await self.client.chat.completions.create(
|
| 54 |
+
model=model_id,
|
| 55 |
+
messages=messages,
|
| 56 |
+
max_tokens=kwargs.get("max_tokens", 256),
|
| 57 |
+
temperature=kwargs.get("temperature", 0.7),
|
| 58 |
+
)
|
| 59 |
+
usage = response.usage
|
| 60 |
+
return {
|
| 61 |
+
"content": response.choices[0].message.content or "",
|
| 62 |
+
"total_tokens": usage.total_tokens if usage else 0,
|
| 63 |
+
"model": model_id,
|
| 64 |
+
}
|