Files changed (3) hide show
  1. Dockerfile +18 -14
  2. app.py +147 -165
  3. requirements.txt +9 -14
Dockerfile CHANGED
@@ -1,17 +1,21 @@
1
- FROM python:3.10-slim
2
-
3
- ENV PYTHONDONTWRITEBYTECODE=1 \
4
- PYTHONUNBUFFERED=1 \
5
- PIP_NO_CACHE_DIR=1 \
6
- PORT=7860 \
7
- SPACE_E4B_URL=https://cloudunity-gemma-e4b.hf.space \
8
- SPACE_MOE_URL=https://cloudunity-gemma-moe26b.hf.space \
9
- SPACE_DENSE_URL=https://cloudunity-gemma-dense31b.hf.space
10
 
11
  WORKDIR /app
12
- RUN apt-get update && apt-get install -y --no-install-recommends git curl build-essential && rm -rf /var/lib/apt/lists/*
13
- COPY requirements.txt /app/requirements.txt
14
- RUN pip install --upgrade pip && pip install -r /app/requirements.txt
15
- COPY app.py /app/app.py
 
 
 
 
 
 
 
 
16
  EXPOSE 7860
17
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
1
+ FROM python:3.11-slim
 
 
 
 
 
 
 
 
2
 
3
  WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ build-essential \
7
+ git \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ COPY requirements_simple.txt requirements.txt
11
+
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ COPY app_simple.py app.py
15
+
16
  EXPOSE 7860
17
+
18
+ # Set model (change this to use different Gemma 4 variant)
19
+ ENV MODEL_NAME=google/gemma-4-E4B-it
20
+
21
+ CMD ["python", "app.py"]
app.py CHANGED
@@ -1,20 +1,22 @@
1
  import os
2
  import logging
3
- import asyncio
4
- import time
5
- import uuid
6
- from typing import Optional, List
7
  from datetime import datetime
 
8
 
9
  from fastapi import FastAPI, HTTPException
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from pydantic import BaseModel, Field
12
- import httpx
 
13
 
 
14
  logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
- app = FastAPI(title="Gemma Orchestrator API - OpenAI Compatible")
 
 
18
  app.add_middleware(
19
  CORSMiddleware,
20
  allow_origins=["*"],
@@ -23,186 +25,166 @@ app.add_middleware(
23
  allow_headers=["*"],
24
  )
25
 
26
- SPACES = {
27
- "gemma-4-e4b": {
28
- "url": os.getenv("SPACE_E4B_URL", "https://example-gemma-e4b.hf.space"),
29
- "model": "gemma-4-E4B-it",
30
- "size_gb": 5,
31
- "context": 128000,
32
- "best_for": ["fast", "multimodal", "edge"],
33
- "latency_ms": 0,
34
- "health": "unknown",
35
- },
36
- "gemma-4-26b-moe": {
37
- "url": os.getenv("SPACE_MOE_URL", "https://example-gemma-moe26b.hf.space"),
38
- "model": "gemma-4-26B-A4B-it",
39
- "size_gb": 18,
40
- "context": 262144,
41
- "best_for": ["balanced", "reasoning", "efficient"],
42
- "latency_ms": 0,
43
- "health": "unknown",
44
- },
45
- "gemma-4-31b": {
46
- "url": os.getenv("SPACE_DENSE_URL", "https://example-gemma-dense31b.hf.space"),
47
- "model": "gemma-4-31B-it",
48
- "size_gb": 20,
49
- "context": 262144,
50
- "best_for": ["complex", "coding", "long-context", "reasoning"],
51
- "latency_ms": 0,
52
- "health": "unknown",
53
- },
54
- }
55
-
56
- class ChatCompletionMessage(BaseModel):
57
  role: str
58
  content: str
59
 
60
- class ChatCompletionRequest(BaseModel):
61
- model: Optional[str] = None
62
- messages: List[ChatCompletionMessage]
63
  temperature: float = Field(default=0.7, ge=0.0, le=2.0)
64
- max_tokens: Optional[int] = Field(default=512, ge=1)
65
  top_p: float = Field(default=0.9, ge=0.0, le=1.0)
66
- top_k: int = Field(default=50, ge=1)
67
- stream: bool = False
68
- n: int = 1
69
 
70
- class ChatCompletionChoice(BaseModel):
71
  index: int
72
- message: ChatCompletionMessage
73
  finish_reason: str
74
 
75
- class ChatCompletionUsage(BaseModel):
76
- prompt_tokens: int
77
  completion_tokens: int
78
  total_tokens: int
79
 
80
- class ChatCompletionResponse(BaseModel):
81
- id: str
82
- object: str = "chat.completion"
83
- created: int
84
  model: str
85
- choices: List[ChatCompletionChoice]
86
- usage: ChatCompletionUsage
87
-
88
- class Model(BaseModel):
89
- id: str
90
- object: str = "model"
91
  created: int
92
- owned_by: str = "gemma"
93
-
94
- class ModelListResponse(BaseModel):
95
- object: str = "list"
96
- data: List[Model]
97
-
98
- class HealthResponse(BaseModel):
99
- status: str
100
- spaces: dict
101
-
102
- def select_model(model_name: Optional[str], messages: List[ChatCompletionMessage]) -> str:
103
- if model_name and model_name in SPACES:
104
- return model_name
105
- prompt_text = " ".join(msg.content for msg in messages).lower()
106
- has_code = any(k in prompt_text for k in ["code", "python", "javascript", "function", "algorithm", "def ", "class "])
107
- has_complex = any(k in prompt_text for k in ["reasoning", "explain", "analyze", "compare", "detailed", "comprehensive"])
108
- is_short = sum(len(msg.content) for msg in messages) < 500
109
- best = "gemma-4-31b" if (has_code or has_complex) else ("gemma-4-e4b" if is_short else "gemma-4-26b-moe")
110
- if SPACES[best]["health"] != "healthy":
111
- for space_name, info in SPACES.items():
112
- if info["health"] == "healthy":
113
- return space_name
114
- return best
115
-
116
- async def check_space_health(space_name: str) -> bool:
117
- space = SPACES[space_name]
118
- try:
119
- async with httpx.AsyncClient(timeout=10) as client:
120
- response = await client.get(f"{space['url']}/health")
121
- if response.status_code == 200:
122
- SPACES[space_name]["health"] = "healthy"
123
- return True
124
- except Exception:
125
- pass
126
- SPACES[space_name]["health"] = "unhealthy"
127
- return False
128
-
129
- async def forward_request(space_name: str, messages: List[ChatCompletionMessage], temperature: float, max_tokens: int, top_p: float, top_k: int):
130
- space_url = SPACES[space_name]["url"]
131
- payload = {
132
- "messages": [{"role": m.role, "content": m.content} for m in messages],
133
- "temperature": temperature,
134
- "max_tokens": max_tokens,
135
- "top_p": top_p,
136
- "top_k": top_k,
137
- }
138
- try:
139
- async with httpx.AsyncClient(timeout=120) as client:
140
- start = time.time()
141
- response = await client.post(f"{space_url}/infer", json=payload)
142
- latency = (time.time() - start) * 1000
143
- SPACES[space_name]["latency_ms"] = latency
144
- response.raise_for_status()
145
- return response.json(), latency
146
- except httpx.TimeoutException:
147
- raise HTTPException(status_code=504, detail=f"Space {space_name} timeout")
148
- except Exception as e:
149
- raise HTTPException(status_code=502, detail=f"Failed to reach space {space_name}: {e}")
150
 
151
  @app.on_event("startup")
152
- async def startup_event():
153
- await asyncio.gather(*[check_space_health(name) for name in SPACES.keys()])
154
-
155
- @app.get("/health", response_model=HealthResponse)
156
- async def health_check():
157
- await asyncio.gather(*[check_space_health(name) for name in SPACES.keys()])
158
- return HealthResponse(status="healthy", spaces={name: info["health"] for name, info in SPACES.items()})
159
-
160
- @app.get("/v1/models", response_model=ModelListResponse)
161
- @app.get("/models", response_model=ModelListResponse)
162
- async def list_models():
163
- now = int(datetime.utcnow().timestamp())
164
- return ModelListResponse(data=[Model(id=model_id, created=now) for model_id in SPACES.keys()])
165
-
166
- @app.post("/v1/chat/completions")
167
- @app.post("/chat/completions")
168
- async def chat_completions(request: ChatCompletionRequest):
169
- selected_model = select_model(request.model, request.messages)
170
- response_data, _latency = await forward_request(
171
- selected_model,
172
- request.messages,
173
- request.temperature,
174
- request.max_tokens or 512,
175
- request.top_p,
176
- request.top_k,
177
- )
178
- completion_id = str(uuid.uuid4())
179
- tokens = int(response_data.get("tokens_used", 0))
180
- return ChatCompletionResponse(
181
- id=f"chatcmpl-{completion_id}",
182
- created=int(datetime.utcnow().timestamp()),
183
- model=selected_model,
184
- choices=[ChatCompletionChoice(index=0, message=ChatCompletionMessage(role="assistant", content=response_data["response"]), finish_reason="stop")],
185
- usage=ChatCompletionUsage(prompt_tokens=0, completion_tokens=tokens, total_tokens=tokens),
186
- )
187
 
188
- @app.get("/info")
189
- async def orchestrator_info():
190
  return {
191
- "orchestrator": "Gemma Orchestrator - OpenAI Compatible",
192
- "version": "1.0",
193
- "spaces": {name: {k: v for k, v in info.items() if k != "url"} for name, info in SPACES.items()},
 
 
 
 
 
 
194
  }
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  @app.get("/")
197
- async def root():
198
  return {
199
- "name": "Gemma Orchestrator API",
200
- "version": "1.0",
201
- "openai_compatible": True,
202
- "endpoints": {
203
- "/v1/models": "List models",
204
- "/v1/chat/completions": "Chat completions",
205
- "/health": "Health check",
206
- "/info": "Orchestrator info"
 
 
 
207
  }
208
  }
 
 
 
 
 
1
  import os
2
  import logging
3
+ from typing import Optional
 
 
 
4
  from datetime import datetime
5
+ import time
6
 
7
  from fastapi import FastAPI, HTTPException
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from pydantic import BaseModel, Field
10
+ import torch
11
+ from transformers import AutoTokenizer, AutoModelForCausalLM
12
 
13
+ # Logging
14
  logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
+ app = FastAPI(title="Gemma 4 Inference API")
18
+
19
+ # CORS
20
  app.add_middleware(
21
  CORSMiddleware,
22
  allow_origins=["*"],
 
25
  allow_headers=["*"],
26
  )
27
 
28
+ # Config
29
+ MODEL_NAME = os.getenv("MODEL_NAME", "google/gemma-4-E4B-it")
30
+
31
+ # Global model/tokenizer
32
+ model = None
33
+ tokenizer = None
34
+
35
+ # Pydantic models
36
+ class Message(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  role: str
38
  content: str
39
 
40
+ class ChatRequest(BaseModel):
41
+ messages: list[Message]
 
42
  temperature: float = Field(default=0.7, ge=0.0, le=2.0)
43
+ max_tokens: int = Field(default=512, ge=1, le=2048)
44
  top_p: float = Field(default=0.9, ge=0.0, le=1.0)
 
 
 
45
 
46
+ class ChatChoice(BaseModel):
47
  index: int
48
+ message: Message
49
  finish_reason: str
50
 
51
+ class ChatUsage(BaseModel):
 
52
  completion_tokens: int
53
  total_tokens: int
54
 
55
+ class ChatResponse(BaseModel):
 
 
 
56
  model: str
57
+ object: str = "chat.completion"
 
 
 
 
 
58
  created: int
59
+ choices: list[ChatChoice]
60
+ usage: ChatUsage
61
+
62
+ def load_model():
63
+ """Load model and tokenizer on startup."""
64
+ global model, tokenizer
65
+
66
+ logger.info(f"Loading {MODEL_NAME}...")
67
+
68
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
69
+
70
+ # Load with 4-bit quantization to fit in 16GB
71
+ model = AutoModelForCausalLM.from_pretrained(
72
+ MODEL_NAME,
73
+ device_map="auto",
74
+ torch_dtype=torch.bfloat16,
75
+ load_in_4bit=True,
76
+ low_cpu_mem_usage=True,
77
+ )
78
+
79
+ logger.info(f" {MODEL_NAME} loaded successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  @app.on_event("startup")
82
+ async def startup():
83
+ load_model()
84
+
85
+ @app.get("/health")
86
+ def health():
87
+ return {"status": "ok", "model": MODEL_NAME}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ @app.get("/v1/models")
90
+ def list_models():
91
  return {
92
+ "object": "list",
93
+ "data": [
94
+ {
95
+ "id": "gemma-4",
96
+ "object": "model",
97
+ "owned_by": "google",
98
+ "created": int(time.time()),
99
+ }
100
+ ]
101
  }
102
 
103
+ @app.post("/v1/chat/completions", response_model=ChatResponse)
104
+ def chat_completions(request: ChatRequest):
105
+ """OpenAI-compatible chat completions endpoint."""
106
+
107
+ try:
108
+ # Build prompt from messages
109
+ prompt = ""
110
+ for msg in request.messages:
111
+ if msg.role == "system":
112
+ prompt += f"<|system|>\n{msg.content}<|end_of_turn|>\n"
113
+ elif msg.role == "user":
114
+ prompt += f"<|user|>\n{msg.content}<|end_of_turn|>\n"
115
+ elif msg.role == "assistant":
116
+ prompt += f"<|assistant|>\n{msg.content}<|end_of_turn|>\n"
117
+
118
+ prompt += "<|assistant|>\n"
119
+
120
+ # Tokenize
121
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
122
+ input_length = inputs.input_ids.shape[1]
123
+
124
+ # Generate
125
+ start_time = time.time()
126
+ outputs = model.generate(
127
+ **inputs,
128
+ max_new_tokens=request.max_tokens,
129
+ temperature=request.temperature,
130
+ top_p=request.top_p,
131
+ do_sample=request.temperature > 0,
132
+ pad_token_id=tokenizer.eos_token_id,
133
+ )
134
+
135
+ # Decode
136
+ full_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
137
+
138
+ # Extract just the response
139
+ if "<|assistant|>" in full_text:
140
+ response_text = full_text.split("<|assistant|>")[-1].strip()
141
+ else:
142
+ response_text = full_text
143
+
144
+ tokens_generated = outputs.shape[1] - input_length
145
+
146
+ return ChatResponse(
147
+ model="gemma-4",
148
+ created=int(time.time()),
149
+ choices=[
150
+ ChatChoice(
151
+ index=0,
152
+ message=Message(role="assistant", content=response_text),
153
+ finish_reason="stop",
154
+ )
155
+ ],
156
+ usage=ChatUsage(
157
+ completion_tokens=tokens_generated,
158
+ total_tokens=tokens_generated,
159
+ ),
160
+ )
161
+
162
+ except Exception as e:
163
+ logger.error(f"Error: {e}")
164
+ raise HTTPException(status_code=500, detail=str(e))
165
+
166
+ @app.post("/chat/completions", response_model=ChatResponse)
167
+ def chat_completions_no_v1(request: ChatRequest):
168
+ """Alias without /v1/ prefix."""
169
+ return chat_completions(request)
170
+
171
  @app.get("/")
172
+ def root():
173
  return {
174
+ "name": "Gemma 4 API",
175
+ "model": MODEL_NAME,
176
+ "docs": "Use /v1/chat/completions for OpenAI compatibility",
177
+ "example": {
178
+ "url": "/v1/chat/completions",
179
+ "method": "POST",
180
+ "body": {
181
+ "messages": [{"role": "user", "content": "Hello"}],
182
+ "temperature": 0.7,
183
+ "max_tokens": 512,
184
+ }
185
  }
186
  }
187
+
188
+ if __name__ == "__main__":
189
+ import uvicorn
190
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,14 +1,9 @@
1
- fastapi
2
- uvicorn
3
- python-multipart
4
- pydantic
5
- requests
6
- httpx
7
- torch
8
- transformers
9
- huggingface-hub
10
- numpy
11
- python-dotenv
12
- sentencepiece
13
- accelerate
14
- openai
 
1
+ fastapi==0.115.6
2
+ uvicorn==0.32.1
3
+ pydantic==2.10.5
4
+ pydantic-settings==2.7.1
5
+ torch==2.6.0 --index-url https://download.pytorch.org/whl/cpu
6
+ transformers==4.48.1
7
+ huggingface-hub==0.26.5
8
+ bitsandbytes==0.44.1
9
+ accelerate==1.2.1