FlameF0X commited on
Commit
fc63019
·
verified ·
1 Parent(s): 5d22ca5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -71
app.py CHANGED
@@ -39,52 +39,70 @@ from transformers import (
39
  # --------------------------------------------------------------------------- #
40
  # Config + model loading
41
  # --------------------------------------------------------------------------- #
42
- MODEL_ID = os.getenv("MODEL_ID", "FlameF0X/TinyGPT")
 
 
 
 
 
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
- # Same call as in your snippet (plus a device); we reuse its model + tokenizer
50
- # for generation so streaming and token accounting work.
51
- pipe = pipeline(
52
- "text-generation",
53
- model=MODEL_ID,
54
- trust_remote_code=True,
55
- device=DEVICE,
56
- )
57
- tokenizer = pipe.tokenizer
58
- model = pipe.model
59
- model.eval()
60
-
61
- # TinyGPT is a GPT-2 model with a 2,048-token context window. Derive the real
62
- # limit from the loaded config instead of guessing or trusting tokenizer sentinels.
63
- _config_context_candidates = [
64
- getattr(model.config, key, None)
65
- for key in ("n_positions", "max_position_embeddings", "n_ctx")
66
- ]
67
- _config_context_candidates = [
68
- int(value) for value in _config_context_candidates
69
- if isinstance(value, (int, float)) and 0 < int(value) < 1_000_000
70
- ]
71
- MODEL_CONTEXT_WINDOW = min(_config_context_candidates) if _config_context_candidates else 2048
72
- _requested_context_window = os.getenv("CONTEXT_WINDOW")
73
- if _requested_context_window:
74
- try:
75
- MODEL_CONTEXT_WINDOW = min(MODEL_CONTEXT_WINDOW, max(128, int(_requested_context_window)))
76
- except ValueError as exc:
77
- raise RuntimeError("CONTEXT_WINDOW must be an integer.") from exc
78
- # Leave room for at least a small prompt, even if a client requests too many tokens.
79
- MAX_OUTPUT_TOKENS = max(1, MODEL_CONTEXT_WINDOW - 64)
80
- IS_TINYGPT = MODEL_ID.rstrip("/").lower() == "flamef0x/tinygpt"
81
-
82
- if tokenizer.pad_token_id is None:
83
- tokenizer.pad_token = tokenizer.eos_token
 
 
 
 
 
 
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 ("system", "developer"):
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(system_text, normalized)
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 very long system prefix or tiny custom 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,17 +443,24 @@ def health():
443
  def list_models():
444
  return {
445
  "object": "list",
446
- "data": [{"id": MODEL_ID, "object": "model", "created": 0, "owned_by": "FlameF0X"}],
 
 
 
447
  }
448
 
449
 
450
  @api.get("/v1/models/{model_id:path}", dependencies=[Depends(auth)])
451
  def get_model(model_id: str):
452
- return {"id": MODEL_ID, "object": "model", "created": 0, "owned_by": "FlameF0X"}
 
 
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 full OpenAI `messages` array on every request,
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, system_prompt, max_tokens, temperature, top_p, top_k, rep_pen):
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 TinyGPT",
653
  description="Conversation history is included automatically on each turn.",
654
  additional_inputs=[
655
- gr.Textbox(label="System prompt", value="", placeholder="Optional behavior or role instructions", lines=2),
 
 
 
 
 
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"),