knighter75 commited on
Commit
ea89131
·
1 Parent(s): d866e5b

Fix actual models

Browse files
Files changed (5) hide show
  1. README.md +32 -3
  2. app/factory.py +8 -20
  3. app/main.py +40 -29
  4. app/models.py +10 -6
  5. 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: LLM proxy
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- _providers = {
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 cls._providers:
18
  raise ValueError(f"Unsupported model: {model_name}")
19
-
20
- provider_class = cls._providers[model_name]
21
- cache_key = provider_class.__name__
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 os
2
- import sys
3
- from fastapi import FastAPI, Depends, HTTPException
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": "1.0.0"}
 
17
 
18
  @app.get("/v1/models")
19
- async def list_models(api_key: str = Depends(verify_api_key)):
20
- """Возвращает список доступных моделей"""
21
  return {
22
- "models": [
23
- {"id": "arch-router", "name": "Arch Router 1.5B (HF)", "provider": "huggingface"},
24
- {"id": "phi-3-mini", "name": "Phi-3 Mini 4K (HF)", "provider": "huggingface"},
25
- {"id": "gemma-2b", "name": "Gemma 2 2B (HF)", "provider": "huggingface"},
26
- {"id": "mistral-7b", "name": "Mistral 7B (HF)", "provider": "huggingface"},
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"chat-{hash(str(request.messages))}",
48
- choices=[{"message": {"content": result["content"]}}],
49
- usage={"total_tokens": result.get("total_tokens", 0)},
50
- model=request.model
 
 
 
 
 
 
51
  )
52
- except ValueError as e:
53
- raise HTTPException(status_code=400, detail=str(e))
54
- except Exception as e:
55
- raise HTTPException(status_code=502, detail=f"Provider error: {str(e)}")
 
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 pydantic import BaseModel
2
- from typing import List, Optional
 
 
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: Optional[int] = 500
12
- temperature: Optional[float] = 0.7
 
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 openai import OpenAI
3
- from typing import List, Dict, Any
 
 
4
  from .base import BaseLLMProvider
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  class HFOpenAIProvider(BaseLLMProvider):
7
- """Провайдер для Hugging Face через OpenAI-совместимый API"""
8
-
9
- SUPPORTED_MODELS = {
10
- "arch-router": "katanemo/Arch-Router-1.5B:hf-inference",
11
- "phi-3-mini": "microsoft/Phi-3-mini-4k-instruct:hf-inference",
12
- "gemma-2b": "google/gemma-2-2b-it:hf-inference",
13
- "mistral-7b": "mistralai/Mistral-7B-Instruct-v0.3:hf-inference",
14
- "llama-3b": "meta-llama/Llama-3.2-3B-Instruct:hf-inference",
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=self.api_key
 
 
26
  )
27
- print(f"🤗 HF OpenAI Provider initialized")
28
-
29
- def _get_model_id(self, model_name: str) -> str:
30
- if model_name in self.SUPPORTED_MODELS:
31
- return self.SUPPORTED_MODELS[model_name]
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
- response = self.client.chat.completions.create(
39
- model=model,
40
- messages=messages,
41
- max_tokens=kwargs.get("max_tokens", 500),
42
- temperature=kwargs.get("temperature", 0.7)
43
- )
44
-
45
- return {
46
- "content": response.choices[0].message.content,
47
- "total_tokens": response.usage.total_tokens if response.usage else 0,
48
- "model": model
49
- }
50
- except Exception as e:
51
- print(f"❌ Error: {e}")
52
- raise
 
 
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
+ }