Update app.py
Browse files
app.py
CHANGED
|
@@ -39,52 +39,70 @@ from transformers import (
|
|
| 39 |
# --------------------------------------------------------------------------- #
|
| 40 |
# Config + model loading
|
| 41 |
# --------------------------------------------------------------------------- #
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
API_KEY = os.getenv("API_KEY") # optional
|
| 44 |
MAX_NEW_TOKENS_LIMIT = int(os.getenv("MAX_NEW_TOKENS", "1024"))
|
| 45 |
DEFAULT_MAX_TOKENS = 256
|
| 46 |
|
| 47 |
DEVICE = 0 if torch.cuda.is_available() else -1
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
tokenizer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
# One generation at a time (small model, usually CPU / single GPU Space).
|
| 86 |
GEN_LOCK = threading.Lock()
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
# --------------------------------------------------------------------------- #
|
| 90 |
# Generation core (shared by the API and the Gradio playground)
|
|
@@ -279,17 +297,16 @@ def messages_to_prompt(
|
|
| 279 |
raise ValueError("`messages` must be a non-empty list.")
|
| 280 |
|
| 281 |
normalized: List[Dict[str, str]] = []
|
| 282 |
-
system_parts: List[str] = []
|
| 283 |
for item in messages:
|
| 284 |
if not isinstance(item, dict):
|
| 285 |
raise ValueError("Each message must be an object with `role` and `content`.")
|
| 286 |
role = str(item.get("role", "user")).strip().lower()
|
| 287 |
content = _flatten_content(item.get("content")).strip()
|
| 288 |
-
if role in ("
|
| 289 |
-
if content:
|
| 290 |
-
system_parts.append(content)
|
| 291 |
-
elif role in ("user", "assistant"):
|
| 292 |
normalized.append({"role": role, "content": content})
|
|
|
|
|
|
|
|
|
|
| 293 |
elif role == "tool":
|
| 294 |
# Tool execution itself is the client's responsibility. Preserve tool
|
| 295 |
# results in the context so the next assistant turn can use them.
|
|
@@ -303,27 +320,21 @@ def messages_to_prompt(
|
|
| 303 |
if not normalized:
|
| 304 |
raise ValueError("`messages` must contain at least one user or assistant message.")
|
| 305 |
|
| 306 |
-
system_text = "\n".join(system_parts)
|
| 307 |
-
|
| 308 |
if not IS_TINYGPT and getattr(tokenizer, "chat_template", None):
|
| 309 |
def render() -> str:
|
| 310 |
template_messages: List[Dict[str, str]] = []
|
| 311 |
-
if system_text:
|
| 312 |
-
template_messages.append({"role": "system", "content": system_text})
|
| 313 |
template_messages.extend(normalized)
|
| 314 |
return tokenizer.apply_chat_template(
|
| 315 |
template_messages, tokenize=False, add_generation_prompt=True
|
| 316 |
)
|
| 317 |
elif IS_TINYGPT:
|
| 318 |
def render() -> str:
|
| 319 |
-
return _render_tinygpt_prompt(
|
| 320 |
else:
|
| 321 |
# Fallback for models without an installed chat template.
|
| 322 |
labels = {"system": "System", "user": "User", "assistant": "Assistant"}
|
| 323 |
def render() -> str:
|
| 324 |
lines = []
|
| 325 |
-
if system_text:
|
| 326 |
-
lines.append(f"System: {system_text}")
|
| 327 |
lines.extend(f"{labels[m['role']]}: {m['content']}" for m in normalized)
|
| 328 |
return "\n".join(lines) + "\nAssistant:"
|
| 329 |
|
|
@@ -331,17 +342,6 @@ def messages_to_prompt(
|
|
| 331 |
if max_input_tokens is None or max_input_tokens < 1:
|
| 332 |
return prompt
|
| 333 |
|
| 334 |
-
# Shorten the system preamble first, but keep its beginning, where instructions
|
| 335 |
-
# such as the assistant's role usually live. Reserve most context for dialogue.
|
| 336 |
-
if system_text and _token_count(prompt) > max_input_tokens:
|
| 337 |
-
system_budget = min(_token_count(system_text), max(0, max_input_tokens // 4))
|
| 338 |
-
if system_budget:
|
| 339 |
-
system_ids = tokenizer.encode(system_text, add_special_tokens=False)[:system_budget]
|
| 340 |
-
system_text = tokenizer.decode(system_ids, skip_special_tokens=True)
|
| 341 |
-
else:
|
| 342 |
-
system_text = ""
|
| 343 |
-
prompt = render()
|
| 344 |
-
|
| 345 |
# Drop the oldest user turn and its assistant reply, never the newest request,
|
| 346 |
# until the remaining transcript fits.
|
| 347 |
while _token_count(prompt) > max_input_tokens and len(normalized) > 1:
|
|
@@ -362,7 +362,7 @@ def messages_to_prompt(
|
|
| 362 |
content_ids = tokenizer.encode(original_content, add_special_tokens=False)
|
| 363 |
last["content"] = tokenizer.decode(content_ids[-available:], skip_special_tokens=True)
|
| 364 |
prompt = render()
|
| 365 |
-
# This can only happen with a
|
| 366 |
if _token_count(prompt) > max_input_tokens:
|
| 367 |
prompt_ids = tokenizer.encode(prompt, add_special_tokens=False)[-max_input_tokens:]
|
| 368 |
prompt = tokenizer.decode(prompt_ids, skip_special_tokens=True)
|
|
@@ -443,17 +443,24 @@ def health():
|
|
| 443 |
def list_models():
|
| 444 |
return {
|
| 445 |
"object": "list",
|
| 446 |
-
"data": [
|
|
|
|
|
|
|
|
|
|
| 447 |
}
|
| 448 |
|
| 449 |
|
| 450 |
@api.get("/v1/models/{model_id:path}", dependencies=[Depends(auth)])
|
| 451 |
def get_model(model_id: str):
|
| 452 |
-
|
|
|
|
|
|
|
| 453 |
|
| 454 |
|
| 455 |
@api.post("/v1/completions", dependencies=[Depends(auth)])
|
| 456 |
def completions(req: CompletionRequest):
|
|
|
|
|
|
|
| 457 |
prompts = [req.prompt] if isinstance(req.prompt, str) else list(req.prompt)
|
| 458 |
if not prompts:
|
| 459 |
raise HTTPException(400, "`prompt` is required.")
|
|
@@ -505,6 +512,8 @@ def completions(req: CompletionRequest):
|
|
| 505 |
|
| 506 |
@api.post("/v1/chat/completions", dependencies=[Depends(auth)])
|
| 507 |
def chat_completions(req: ChatRequest):
|
|
|
|
|
|
|
| 508 |
if not req.messages:
|
| 509 |
raise HTTPException(400, "`messages` is required.")
|
| 510 |
p = req.to_params()
|
|
@@ -574,9 +583,7 @@ DOCS = f"""
|
|
| 574 |
**Context window:** {MODEL_CONTEXT_WINDOW} tokens
|
| 575 |
**API key:** {"required (Space secret `API_KEY`)" if API_KEY else "not required (set the `API_KEY` secret to enable authentication)"}
|
| 576 |
|
| 577 |
-
The Chat Completions endpoint accepts the
|
| 578 |
-
including previous user and assistant turns. The built-in chat tab keeps conversation history
|
| 579 |
-
for you. TinyGPT chat history is formatted as `User: ...` / `Assistant: ...`. For TinyGPT, generation stops at `User:` and that marker is never streamed to clients.
|
| 580 |
Extra sampling parameters supported by the API: `top_k`, `repetition_penalty`, and `seed`.
|
| 581 |
"""
|
| 582 |
|
|
@@ -593,12 +600,11 @@ def _chat_ui_params(max_tokens, temperature, top_p, top_k, rep_pen):
|
|
| 593 |
}
|
| 594 |
|
| 595 |
|
| 596 |
-
def chat_playground(message, history,
|
| 597 |
"""Gradio's ChatInterface callback; `history` contains all earlier turns."""
|
|
|
|
|
|
|
| 598 |
messages: List[Dict[str, Any]] = []
|
| 599 |
-
if system_prompt and str(system_prompt).strip():
|
| 600 |
-
messages.append({"role": "system", "content": str(system_prompt).strip()})
|
| 601 |
-
|
| 602 |
for item in history or []:
|
| 603 |
if isinstance(item, dict) and item.get("role") in ("user", "assistant"):
|
| 604 |
messages.append({"role": item["role"], "content": _flatten_content(item.get("content"))})
|
|
@@ -649,10 +655,15 @@ with gr.Blocks(title=f"{MODEL_ID} API") as demo:
|
|
| 649 |
with gr.Tab("Multi-turn chat"):
|
| 650 |
gr.ChatInterface(
|
| 651 |
fn=chat_playground,
|
| 652 |
-
title="Chat with
|
| 653 |
description="Conversation history is included automatically on each turn.",
|
| 654 |
additional_inputs=[
|
| 655 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
gr.Slider(1, min(MAX_OUTPUT_TOKENS, MAX_NEW_TOKENS_LIMIT), value=min(256, MAX_OUTPUT_TOKENS, MAX_NEW_TOKENS_LIMIT), step=1, label="max_tokens"),
|
| 657 |
gr.Slider(0, 2, value=0.8, step=0.05, label="temperature (0 = greedy)"),
|
| 658 |
gr.Slider(0.05, 1, value=0.9, step=0.05, label="top_p"),
|
|
|
|
| 39 |
# --------------------------------------------------------------------------- #
|
| 40 |
# Config + model loading
|
| 41 |
# --------------------------------------------------------------------------- #
|
| 42 |
+
DEFAULT_MODEL_ID = os.getenv("MODEL_ID", "FlameF0X/TinyGPT")
|
| 43 |
+
AVAILABLE_MODELS = [
|
| 44 |
+
"FlameF0X/TinyGPT",
|
| 45 |
+
"FWKV/Myosotis-1.5-base-Preview",
|
| 46 |
+
]
|
| 47 |
+
MODEL_ID = DEFAULT_MODEL_ID if DEFAULT_MODEL_ID in AVAILABLE_MODELS else "FlameF0X/TinyGPT"
|
| 48 |
API_KEY = os.getenv("API_KEY") # optional
|
| 49 |
MAX_NEW_TOKENS_LIMIT = int(os.getenv("MAX_NEW_TOKENS", "1024"))
|
| 50 |
DEFAULT_MAX_TOKENS = 256
|
| 51 |
|
| 52 |
DEVICE = 0 if torch.cuda.is_available() else -1
|
| 53 |
|
| 54 |
+
def _context_window_for(loaded_model) -> int:
|
| 55 |
+
candidates = [
|
| 56 |
+
getattr(loaded_model.config, key, None)
|
| 57 |
+
for key in ("n_positions", "max_position_embeddings", "n_ctx", "max_sequence_length")
|
| 58 |
+
]
|
| 59 |
+
candidates = [
|
| 60 |
+
int(value) for value in candidates
|
| 61 |
+
if isinstance(value, (int, float)) and 0 < int(value) < 1_000_000
|
| 62 |
+
]
|
| 63 |
+
context_window = min(candidates) if candidates else 2048
|
| 64 |
+
requested = os.getenv("CONTEXT_WINDOW")
|
| 65 |
+
if requested:
|
| 66 |
+
try:
|
| 67 |
+
context_window = min(context_window, max(128, int(requested)))
|
| 68 |
+
except ValueError as exc:
|
| 69 |
+
raise RuntimeError("CONTEXT_WINDOW must be an integer.") from exc
|
| 70 |
+
return context_window
|
| 71 |
+
|
| 72 |
+
def _load_model(model_id: str):
|
| 73 |
+
global pipe, tokenizer, model, MODEL_ID, MODEL_CONTEXT_WINDOW
|
| 74 |
+
global MAX_OUTPUT_TOKENS, IS_TINYGPT
|
| 75 |
+
if model_id not in AVAILABLE_MODELS:
|
| 76 |
+
raise ValueError(f"Unsupported model: {model_id}")
|
| 77 |
+
new_pipe = pipeline(
|
| 78 |
+
"text-generation",
|
| 79 |
+
model=model_id,
|
| 80 |
+
trust_remote_code=True,
|
| 81 |
+
device=DEVICE,
|
| 82 |
+
)
|
| 83 |
+
new_tokenizer = new_pipe.tokenizer
|
| 84 |
+
new_model = new_pipe.model
|
| 85 |
+
new_model.eval()
|
| 86 |
+
if new_tokenizer.pad_token_id is None:
|
| 87 |
+
new_tokenizer.pad_token = new_tokenizer.eos_token
|
| 88 |
+
pipe, tokenizer, model = new_pipe, new_tokenizer, new_model
|
| 89 |
+
MODEL_ID = model_id
|
| 90 |
+
MODEL_CONTEXT_WINDOW = _context_window_for(model)
|
| 91 |
+
MAX_OUTPUT_TOKENS = max(1, MODEL_CONTEXT_WINDOW - 64)
|
| 92 |
+
IS_TINYGPT = MODEL_ID.rstrip("/").lower() == "flamef0x/tinygpt"
|
| 93 |
+
|
| 94 |
+
_load_model(MODEL_ID)
|
| 95 |
|
| 96 |
# One generation at a time (small model, usually CPU / single GPU Space).
|
| 97 |
GEN_LOCK = threading.Lock()
|
| 98 |
|
| 99 |
+
def select_model(model_id: str):
|
| 100 |
+
# Avoid swapping tokenizer/model while a generation is using them.
|
| 101 |
+
with GEN_LOCK:
|
| 102 |
+
_load_model(model_id)
|
| 103 |
+
return f"Active model: {MODEL_ID}"
|
| 104 |
+
|
| 105 |
+
|
| 106 |
|
| 107 |
# --------------------------------------------------------------------------- #
|
| 108 |
# Generation core (shared by the API and the Gradio playground)
|
|
|
|
| 297 |
raise ValueError("`messages` must be a non-empty list.")
|
| 298 |
|
| 299 |
normalized: List[Dict[str, str]] = []
|
|
|
|
| 300 |
for item in messages:
|
| 301 |
if not isinstance(item, dict):
|
| 302 |
raise ValueError("Each message must be an object with `role` and `content`.")
|
| 303 |
role = str(item.get("role", "user")).strip().lower()
|
| 304 |
content = _flatten_content(item.get("content")).strip()
|
| 305 |
+
if role in ("user", "assistant"):
|
|
|
|
|
|
|
|
|
|
| 306 |
normalized.append({"role": role, "content": content})
|
| 307 |
+
elif role in ("system", "developer"):
|
| 308 |
+
# System/developer prompts are intentionally ignored.
|
| 309 |
+
continue
|
| 310 |
elif role == "tool":
|
| 311 |
# Tool execution itself is the client's responsibility. Preserve tool
|
| 312 |
# results in the context so the next assistant turn can use them.
|
|
|
|
| 320 |
if not normalized:
|
| 321 |
raise ValueError("`messages` must contain at least one user or assistant message.")
|
| 322 |
|
|
|
|
|
|
|
| 323 |
if not IS_TINYGPT and getattr(tokenizer, "chat_template", None):
|
| 324 |
def render() -> str:
|
| 325 |
template_messages: List[Dict[str, str]] = []
|
|
|
|
|
|
|
| 326 |
template_messages.extend(normalized)
|
| 327 |
return tokenizer.apply_chat_template(
|
| 328 |
template_messages, tokenize=False, add_generation_prompt=True
|
| 329 |
)
|
| 330 |
elif IS_TINYGPT:
|
| 331 |
def render() -> str:
|
| 332 |
+
return _render_tinygpt_prompt("", normalized)
|
| 333 |
else:
|
| 334 |
# Fallback for models without an installed chat template.
|
| 335 |
labels = {"system": "System", "user": "User", "assistant": "Assistant"}
|
| 336 |
def render() -> str:
|
| 337 |
lines = []
|
|
|
|
|
|
|
| 338 |
lines.extend(f"{labels[m['role']]}: {m['content']}" for m in normalized)
|
| 339 |
return "\n".join(lines) + "\nAssistant:"
|
| 340 |
|
|
|
|
| 342 |
if max_input_tokens is None or max_input_tokens < 1:
|
| 343 |
return prompt
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
# Drop the oldest user turn and its assistant reply, never the newest request,
|
| 346 |
# until the remaining transcript fits.
|
| 347 |
while _token_count(prompt) > max_input_tokens and len(normalized) > 1:
|
|
|
|
| 362 |
content_ids = tokenizer.encode(original_content, add_special_tokens=False)
|
| 363 |
last["content"] = tokenizer.decode(content_ids[-available:], skip_special_tokens=True)
|
| 364 |
prompt = render()
|
| 365 |
+
# This can only happen with a tiny custom context limit.
|
| 366 |
if _token_count(prompt) > max_input_tokens:
|
| 367 |
prompt_ids = tokenizer.encode(prompt, add_special_tokens=False)[-max_input_tokens:]
|
| 368 |
prompt = tokenizer.decode(prompt_ids, skip_special_tokens=True)
|
|
|
|
| 443 |
def list_models():
|
| 444 |
return {
|
| 445 |
"object": "list",
|
| 446 |
+
"data": [
|
| 447 |
+
{"id": model_id, "object": "model", "created": 0, "owned_by": model_id.split("/")[0]}
|
| 448 |
+
for model_id in AVAILABLE_MODELS
|
| 449 |
+
],
|
| 450 |
}
|
| 451 |
|
| 452 |
|
| 453 |
@api.get("/v1/models/{model_id:path}", dependencies=[Depends(auth)])
|
| 454 |
def get_model(model_id: str):
|
| 455 |
+
if model_id not in AVAILABLE_MODELS:
|
| 456 |
+
raise HTTPException(404, "Model not found.")
|
| 457 |
+
return {"id": model_id, "object": "model", "created": 0, "owned_by": model_id.split("/")[0]}
|
| 458 |
|
| 459 |
|
| 460 |
@api.post("/v1/completions", dependencies=[Depends(auth)])
|
| 461 |
def completions(req: CompletionRequest):
|
| 462 |
+
if req.model in AVAILABLE_MODELS and req.model != MODEL_ID:
|
| 463 |
+
select_model(req.model)
|
| 464 |
prompts = [req.prompt] if isinstance(req.prompt, str) else list(req.prompt)
|
| 465 |
if not prompts:
|
| 466 |
raise HTTPException(400, "`prompt` is required.")
|
|
|
|
| 512 |
|
| 513 |
@api.post("/v1/chat/completions", dependencies=[Depends(auth)])
|
| 514 |
def chat_completions(req: ChatRequest):
|
| 515 |
+
if req.model in AVAILABLE_MODELS and req.model != MODEL_ID:
|
| 516 |
+
select_model(req.model)
|
| 517 |
if not req.messages:
|
| 518 |
raise HTTPException(400, "`messages` is required.")
|
| 519 |
p = req.to_params()
|
|
|
|
| 583 |
**Context window:** {MODEL_CONTEXT_WINDOW} tokens
|
| 584 |
**API key:** {"required (Space secret `API_KEY`)" if API_KEY else "not required (set the `API_KEY` secret to enable authentication)"}
|
| 585 |
|
| 586 |
+
Available models: {", ".join(AVAILABLE_MODELS)}. The Chat Completions endpoint accepts the OpenAI `messages` array on every request. System and developer messages are ignored. The built-in chat tab keeps conversation history for you. TinyGPT chat history is formatted as `User: ...` / `Assistant: ...`; generation stops at `User:` for TinyGPT.
|
|
|
|
|
|
|
| 587 |
Extra sampling parameters supported by the API: `top_k`, `repetition_penalty`, and `seed`.
|
| 588 |
"""
|
| 589 |
|
|
|
|
| 600 |
}
|
| 601 |
|
| 602 |
|
| 603 |
+
def chat_playground(message, history, model_choice, max_tokens, temperature, top_p, top_k, rep_pen):
|
| 604 |
"""Gradio's ChatInterface callback; `history` contains all earlier turns."""
|
| 605 |
+
if model_choice and model_choice != MODEL_ID:
|
| 606 |
+
select_model(model_choice)
|
| 607 |
messages: List[Dict[str, Any]] = []
|
|
|
|
|
|
|
|
|
|
| 608 |
for item in history or []:
|
| 609 |
if isinstance(item, dict) and item.get("role") in ("user", "assistant"):
|
| 610 |
messages.append({"role": item["role"], "content": _flatten_content(item.get("content"))})
|
|
|
|
| 655 |
with gr.Tab("Multi-turn chat"):
|
| 656 |
gr.ChatInterface(
|
| 657 |
fn=chat_playground,
|
| 658 |
+
title="Chat with selected model",
|
| 659 |
description="Conversation history is included automatically on each turn.",
|
| 660 |
additional_inputs=[
|
| 661 |
+
gr.Dropdown(
|
| 662 |
+
choices=AVAILABLE_MODELS,
|
| 663 |
+
value=MODEL_ID,
|
| 664 |
+
label="Model",
|
| 665 |
+
interactive=True,
|
| 666 |
+
),
|
| 667 |
gr.Slider(1, min(MAX_OUTPUT_TOKENS, MAX_NEW_TOKENS_LIMIT), value=min(256, MAX_OUTPUT_TOKENS, MAX_NEW_TOKENS_LIMIT), step=1, label="max_tokens"),
|
| 668 |
gr.Slider(0, 2, value=0.8, step=0.05, label="temperature (0 = greedy)"),
|
| 669 |
gr.Slider(0.05, 1, value=0.9, step=0.05, label="top_p"),
|