sarajane commited on
Commit
84bf033
·
verified ·
1 Parent(s): d0fe01f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +213 -0
app.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import uuid
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import torch
7
+ from fastapi import FastAPI, Header, HTTPException
8
+ from fastapi.responses import PlainTextResponse
9
+ from pydantic import BaseModel
10
+ from transformers import AutoModelForCausalLM, AutoTokenizer
11
+
12
+
13
+ MODEL_ID = os.environ.get(
14
+ "MODEL_ID",
15
+ "Polygl0t/Tucano2-qwen-0.5B-Instruct"
16
+ )
17
+
18
+ API_KEY = os.environ.get("API_KEY", "")
19
+
20
+ MAX_INPUT_TOKENS = int(os.environ.get("MAX_INPUT_TOKENS", "2048"))
21
+ DEFAULT_MAX_NEW_TOKENS = int(os.environ.get("DEFAULT_MAX_NEW_TOKENS", "80"))
22
+
23
+ tokenizer = None
24
+ model = None
25
+
26
+ app = FastAPI(title="Tucano OpenAI-Compatible API")
27
+
28
+
29
+ class Message(BaseModel):
30
+ role: str
31
+ content: str
32
+
33
+
34
+ class ChatCompletionRequest(BaseModel):
35
+ model: Optional[str] = None
36
+ messages: List[Message]
37
+ temperature: Optional[float] = 0.1
38
+ max_tokens: Optional[int] = DEFAULT_MAX_NEW_TOKENS
39
+ top_p: Optional[float] = 0.95
40
+ stream: Optional[bool] = False
41
+
42
+
43
+ def check_auth(authorization: Optional[str]) -> None:
44
+ if not API_KEY:
45
+ return
46
+
47
+ if not authorization:
48
+ raise HTTPException(status_code=401, detail="Missing Authorization header")
49
+
50
+ expected = f"Bearer {API_KEY}"
51
+
52
+ if authorization != expected:
53
+ raise HTTPException(status_code=403, detail="Invalid API key")
54
+
55
+
56
+ @app.on_event("startup")
57
+ def load_model() -> None:
58
+ global tokenizer, model
59
+
60
+ tokenizer = AutoTokenizer.from_pretrained(
61
+ MODEL_ID,
62
+ trust_remote_code=True
63
+ )
64
+
65
+ model = AutoModelForCausalLM.from_pretrained(
66
+ MODEL_ID,
67
+ torch_dtype=torch.float32,
68
+ low_cpu_mem_usage=True,
69
+ trust_remote_code=True
70
+ )
71
+
72
+ model.eval()
73
+
74
+
75
+ @app.get("/", response_class=PlainTextResponse)
76
+ def root() -> str:
77
+ return "Tucano OpenAI-compatible API is running."
78
+
79
+
80
+ @app.get("/health")
81
+ def health() -> Dict[str, str]:
82
+ return {
83
+ "status": "ok",
84
+ "model": MODEL_ID
85
+ }
86
+
87
+
88
+ @app.get("/v1/models")
89
+ def list_models(
90
+ authorization: Optional[str] = Header(default=None)
91
+ ) -> Dict[str, Any]:
92
+ check_auth(authorization)
93
+
94
+ return {
95
+ "object": "list",
96
+ "data": [
97
+ {
98
+ "id": MODEL_ID,
99
+ "object": "model",
100
+ "created": 0,
101
+ "owned_by": "huggingface-space"
102
+ }
103
+ ]
104
+ }
105
+
106
+
107
+ @app.post("/v1/chat/completions")
108
+ def chat_completions(
109
+ request: ChatCompletionRequest,
110
+ authorization: Optional[str] = Header(default=None)
111
+ ) -> Dict[str, Any]:
112
+ check_auth(authorization)
113
+
114
+ if request.stream:
115
+ raise HTTPException(
116
+ status_code=400,
117
+ detail="Streaming is not supported by this pilot endpoint"
118
+ )
119
+
120
+ if not request.messages:
121
+ raise HTTPException(
122
+ status_code=400,
123
+ detail="At least one message is required"
124
+ )
125
+
126
+ messages = [
127
+ {
128
+ "role": m.role,
129
+ "content": m.content
130
+ }
131
+ for m in request.messages
132
+ ]
133
+
134
+ try:
135
+ prompt = tokenizer.apply_chat_template(
136
+ messages,
137
+ tokenize=False,
138
+ add_generation_prompt=True
139
+ )
140
+ except Exception:
141
+ prompt = ""
142
+
143
+ for m in messages:
144
+ prompt += f"{m['role']}: {m['content']}\n"
145
+
146
+ prompt += "assistant:"
147
+
148
+ inputs = tokenizer(
149
+ prompt,
150
+ return_tensors="pt",
151
+ truncation=True,
152
+ max_length=MAX_INPUT_TOKENS
153
+ )
154
+
155
+ max_new_tokens = request.max_tokens or DEFAULT_MAX_NEW_TOKENS
156
+
157
+ generation_args = {
158
+ "input_ids": inputs["input_ids"],
159
+ "attention_mask": inputs.get("attention_mask"),
160
+ "max_new_tokens": max_new_tokens,
161
+ "pad_token_id": tokenizer.eos_token_id,
162
+ }
163
+
164
+ if request.temperature is not None and request.temperature > 0:
165
+ generation_args["do_sample"] = True
166
+ generation_args["temperature"] = request.temperature
167
+ generation_args["top_p"] = request.top_p or 0.95
168
+ else:
169
+ generation_args["do_sample"] = False
170
+
171
+ start_time = time.time()
172
+
173
+ with torch.no_grad():
174
+ output_ids = model.generate(**generation_args)
175
+
176
+ elapsed = time.time() - start_time
177
+
178
+ prompt_length = inputs["input_ids"].shape[-1]
179
+ generated_ids = output_ids[0][prompt_length:]
180
+
181
+ content = tokenizer.decode(
182
+ generated_ids,
183
+ skip_special_tokens=True
184
+ ).strip()
185
+
186
+ prompt_tokens = int(prompt_length)
187
+ completion_tokens = int(generated_ids.shape[-1])
188
+
189
+ return {
190
+ "id": f"chatcmpl-{uuid.uuid4().hex}",
191
+ "object": "chat.completion",
192
+ "created": int(time.time()),
193
+ "model": request.model or MODEL_ID,
194
+ "choices": [
195
+ {
196
+ "index": 0,
197
+ "message": {
198
+ "role": "assistant",
199
+ "content": content
200
+ },
201
+ "finish_reason": "stop"
202
+ }
203
+ ],
204
+ "usage": {
205
+ "prompt_tokens": prompt_tokens,
206
+ "completion_tokens": completion_tokens,
207
+ "total_tokens": prompt_tokens + completion_tokens
208
+ },
209
+ "pilot_metadata": {
210
+ "served_model": MODEL_ID,
211
+ "elapsed_seconds": round(elapsed, 3)
212
+ }
213
+ }