fomext commited on
Commit
ae48ea7
·
verified ·
1 Parent(s): c01d49a

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -144
app.py CHANGED
@@ -1,17 +1,12 @@
1
- import asyncio
2
  import json
3
  import time
4
  import uuid
5
  from threading import Thread
6
- from typing import AsyncIterator, Optional
7
 
8
  import gradio as gr
9
  import spaces
10
  import torch
11
- import uvicorn
12
- from fastapi import FastAPI, HTTPException
13
- from fastapi.responses import JSONResponse, StreamingResponse
14
- from pydantic import BaseModel, Field
15
  from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
16
 
17
  # ---------------------------------------------------------------------------
@@ -33,53 +28,9 @@ model = AutoModelForCausalLM.from_pretrained(
33
  model.eval()
34
  print("Model ready.")
35
 
36
- # ---------------------------------------------------------------------------
37
- # Pydantic schemas
38
- # ---------------------------------------------------------------------------
39
-
40
-
41
- class ChatMessage(BaseModel):
42
- role: str
43
- content: str
44
-
45
-
46
- class ChatCompletionRequest(BaseModel):
47
- model: str = MODEL_ALIAS
48
- messages: list[ChatMessage]
49
- max_tokens: Optional[int] = Field(default=512)
50
- temperature: Optional[float] = Field(default=0.7)
51
- top_p: Optional[float] = Field(default=0.9)
52
- stream: Optional[bool] = Field(default=False)
53
- enable_thinking: Optional[bool] = Field(default=False)
54
-
55
-
56
- # ---------------------------------------------------------------------------
57
- # Helpers
58
- # ---------------------------------------------------------------------------
59
-
60
-
61
- def build_prompt(messages: list[ChatMessage], enable_thinking: bool) -> str:
62
- hf_messages = [{"role": m.role, "content": m.content} for m in messages]
63
- return tokenizer.apply_chat_template(
64
- hf_messages,
65
- tokenize=False,
66
- add_generation_prompt=True,
67
- enable_thinking=enable_thinking,
68
- )
69
-
70
-
71
- def make_generation_kwargs(request: ChatCompletionRequest) -> dict:
72
- return dict(
73
- max_new_tokens=request.max_tokens or 512,
74
- temperature=request.temperature if request.temperature is not None else 0.7,
75
- top_p=request.top_p if request.top_p is not None else 0.9,
76
- do_sample=True,
77
- pad_token_id=tokenizer.eos_token_id,
78
- )
79
-
80
 
81
  # ---------------------------------------------------------------------------
82
- # GPU generation functions — these are the ZeroGPU anchors.
83
  # ---------------------------------------------------------------------------
84
 
85
 
@@ -121,127 +72,119 @@ def _generate_streaming(prompt: str, gen_kwargs: dict, streamer: TextIteratorStr
121
 
122
 
123
  # ---------------------------------------------------------------------------
124
- # OpenAI response builders
125
  # ---------------------------------------------------------------------------
126
 
127
 
128
- def chat_completion_object(content: str, model_name: str, completion_id: Optional[str] = None) -> dict:
129
- cid = completion_id or f"chatcmpl-{uuid.uuid4().hex}"
130
- return {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  "id": cid,
132
  "object": "chat.completion",
133
  "created": int(time.time()),
134
- "model": model_name,
135
  "choices": [{"index": 0, "message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
136
  "usage": {"prompt_tokens": -1, "completion_tokens": -1, "total_tokens": -1},
137
  }
 
138
 
139
 
140
- def stream_chunk(delta_content: str, model_name: str, completion_id: str, finish_reason: Optional[str] = None) -> str:
141
- chunk = {
142
- "id": completion_id,
143
- "object": "chat.completion.chunk",
144
- "created": int(time.time()),
145
- "model": model_name,
146
- "choices": [{"index": 0, "delta": {"content": delta_content} if delta_content else {}, "finish_reason": finish_reason}],
147
- }
148
- return f"data: {json.dumps(chunk)}\n\n"
149
 
150
 
151
  # ---------------------------------------------------------------------------
152
- # Gradio UI
153
  # ---------------------------------------------------------------------------
154
 
155
  with gr.Blocks(title=f"{MODEL_ALIAS} API") as demo:
156
  gr.Markdown(f"""
157
- # {MODEL_ALIAS} — OpenAI-compatible API
158
 
159
- Point **Paperclip** at `https://<your-space>.hf.space` with model `{MODEL_ALIAS}`.
160
 
161
- | Method | Path | Description |
162
- |--------|------|-------------|
163
- | GET | `/v1/models` | List models |
164
- | POST | `/v1/chat/completions` | Chat (streaming & non-streaming) |
165
- | GET | `/health` | Health check |
166
 
167
  You can also chat directly below.
168
  """)
169
  gr.ChatInterface(fn=gradio_chat)
170
 
171
- # ---------------------------------------------------------------------------
172
- # FastAPI app — built ourselves so the routes are guaranteed to exist before
173
- # Gradio is mounted into it. (demo.app does not exist until demo.launch()
174
- # runs, so routes can never be attached to it beforehand.)
175
- # ---------------------------------------------------------------------------
176
-
177
- app = FastAPI(title="Qwen3-30B-A3B OpenAI-compatible API")
178
-
179
-
180
- @app.get("/v1/models")
181
- async def list_models():
182
- return {
183
- "object": "list",
184
- "data": [{"id": MODEL_ALIAS, "object": "model", "created": int(time.time()), "owned_by": "qwen"}],
185
- }
186
-
187
-
188
- @app.post("/v1/chat/completions")
189
- async def chat_completions(request: ChatCompletionRequest):
190
- try:
191
- prompt = build_prompt(request.messages, request.enable_thinking or False)
192
- gen_kwargs = make_generation_kwargs(request)
193
- except Exception as exc:
194
- raise HTTPException(status_code=422, detail=str(exc))
195
-
196
- if request.stream:
197
- completion_id = f"chatcmpl-{uuid.uuid4().hex}"
198
-
199
- async def token_generator() -> AsyncIterator[str]:
200
- role_chunk = {
201
- "id": completion_id, "object": "chat.completion.chunk",
202
- "created": int(time.time()), "model": request.model,
203
- "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
204
- }
205
- yield f"data: {json.dumps(role_chunk)}\n\n"
206
-
207
- streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
208
- thread = Thread(target=_generate_streaming, args=(prompt, gen_kwargs, streamer), daemon=True)
209
- thread.start()
210
-
211
- try:
212
- for token_text in streamer:
213
- if token_text:
214
- yield stream_chunk(token_text, request.model, completion_id)
215
- await asyncio.sleep(0)
216
- finally:
217
- thread.join()
218
-
219
- yield stream_chunk("", request.model, completion_id, finish_reason="stop")
220
- yield "data: [DONE]\n\n"
221
-
222
- return StreamingResponse(token_generator(), media_type="text/event-stream",
223
- headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
224
-
225
- try:
226
- content = _generate_response(prompt, gen_kwargs)
227
- except Exception as exc:
228
- raise HTTPException(status_code=500, detail=f"Generation failed: {exc}")
229
-
230
- return JSONResponse(chat_completion_object(content, request.model))
231
-
232
-
233
- @app.get("/health")
234
- async def health():
235
- return {"status": "ok", "model": MODEL_ID}
236
 
237
 
238
  # ---------------------------------------------------------------------------
239
- # Mount Gradio into our FastAPI app at the ROOT path. ZeroGPU's scanner
240
- # inspects the module for @spaces.GPU usage — it does not require
241
- # demo.launch() to be called, so this mount-and-uvicorn pattern is safe.
242
  # ---------------------------------------------------------------------------
243
 
244
- app = gr.mount_gradio_app(app, demo, path="/")
245
-
246
  if __name__ == "__main__":
247
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
1
  import json
2
  import time
3
  import uuid
4
  from threading import Thread
5
+ from typing import Optional
6
 
7
  import gradio as gr
8
  import spaces
9
  import torch
 
 
 
 
10
  from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
11
 
12
  # ---------------------------------------------------------------------------
 
28
  model.eval()
29
  print("Model ready.")
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  # ---------------------------------------------------------------------------
33
+ # GPU generation functions — ZeroGPU anchors
34
  # ---------------------------------------------------------------------------
35
 
36
 
 
72
 
73
 
74
  # ---------------------------------------------------------------------------
75
+ # API functions — exposed via gr.api()
76
  # ---------------------------------------------------------------------------
77
 
78
 
79
+ def list_models() -> str:
80
+ """Returns a JSON string listing available models."""
81
+ result = {
82
+ "object": "list",
83
+ "data": [{"id": MODEL_ALIAS, "object": "model", "created": int(time.time()), "owned_by": "qwen"}],
84
+ }
85
+ return json.dumps(result)
86
+
87
+
88
+ def chat_completions(
89
+ messages_json: str,
90
+ max_tokens: int = 512,
91
+ temperature: float = 0.7,
92
+ top_p: float = 0.9,
93
+ enable_thinking: bool = False,
94
+ ) -> str:
95
+ """
96
+ Non-streaming chat completions. Returns an OpenAI-compatible JSON string.
97
+
98
+ Args:
99
+ messages_json: JSON array of {role, content} objects, e.g.
100
+ '[{"role":"user","content":"Hello"}]'
101
+ max_tokens: Maximum tokens to generate (default 512).
102
+ temperature: Sampling temperature (default 0.7).
103
+ top_p: Nucleus sampling probability (default 0.9).
104
+ enable_thinking: Enable chain-of-thought thinking (default False).
105
+
106
+ Returns:
107
+ OpenAI-compatible chat completion JSON string.
108
+ """
109
+ try:
110
+ messages = json.loads(messages_json)
111
+ except json.JSONDecodeError as e:
112
+ return json.dumps({"error": f"Invalid messages_json: {e}"})
113
+
114
+ try:
115
+ hf_messages = [{"role": m["role"], "content": m["content"]} for m in messages]
116
+ prompt = tokenizer.apply_chat_template(
117
+ hf_messages,
118
+ tokenize=False,
119
+ add_generation_prompt=True,
120
+ enable_thinking=enable_thinking,
121
+ )
122
+ except Exception as e:
123
+ return json.dumps({"error": f"Prompt build failed: {e}"})
124
+
125
+ gen_kwargs = dict(
126
+ max_new_tokens=max_tokens,
127
+ temperature=temperature,
128
+ top_p=top_p,
129
+ do_sample=True,
130
+ pad_token_id=tokenizer.eos_token_id,
131
+ )
132
+
133
+ try:
134
+ content = _generate_response(prompt, gen_kwargs)
135
+ except Exception as e:
136
+ return json.dumps({"error": f"Generation failed: {e}"})
137
+
138
+ cid = f"chatcmpl-{uuid.uuid4().hex}"
139
+ result = {
140
  "id": cid,
141
  "object": "chat.completion",
142
  "created": int(time.time()),
143
+ "model": MODEL_ALIAS,
144
  "choices": [{"index": 0, "message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
145
  "usage": {"prompt_tokens": -1, "completion_tokens": -1, "total_tokens": -1},
146
  }
147
+ return json.dumps(result)
148
 
149
 
150
+ def health() -> str:
151
+ """Returns a JSON health-check string."""
152
+ return json.dumps({"status": "ok", "model": MODEL_ID})
 
 
 
 
 
 
153
 
154
 
155
  # ---------------------------------------------------------------------------
156
+ # Gradio UI + API
157
  # ---------------------------------------------------------------------------
158
 
159
  with gr.Blocks(title=f"{MODEL_ALIAS} API") as demo:
160
  gr.Markdown(f"""
161
+ # {MODEL_ALIAS} — Gradio API
162
 
163
+ Use the Gradio API endpoint at `/gradio_api/call/<fn_name>`.
164
 
165
+ | Function | Description |
166
+ |----------|-------------|
167
+ | `list_models` | List available models (returns JSON string) |
168
+ | `chat_completions` | Chat completions, non-streaming (returns JSON string) |
169
+ | `health` | Health check (returns JSON string) |
170
 
171
  You can also chat directly below.
172
  """)
173
  gr.ChatInterface(fn=gradio_chat)
174
 
175
+ # Expose API functions — endpoints appear at /gradio_api/call/<fn_name>
176
+ gr.api(list_models, api_name="list_models")
177
+ gr.api(chat_completions, api_name="chat_completions")
178
+ gr.api(health, api_name="health")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
 
181
  # ---------------------------------------------------------------------------
182
+ # Entry-point
 
 
183
  # ---------------------------------------------------------------------------
184
 
 
 
185
  if __name__ == "__main__":
186
+ demo.queue()
187
+ demo.launch(
188
+ server_name="0.0.0.0",
189
+ server_port=7860,
190
+ )