File size: 7,133 Bytes
3bb28fd
 
 
 
 
 
 
 
 
b8302d9
3bb28fd
 
 
 
 
 
 
 
 
 
 
 
 
9f1c50c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3bb28fd
 
 
 
 
 
 
 
 
 
 
 
 
9f1c50c
 
3bb28fd
9f1c50c
3bb28fd
 
 
 
 
 
 
 
 
 
 
 
 
 
9f1c50c
 
 
 
3bb28fd
 
 
 
 
 
 
 
 
 
 
 
 
9f1c50c
3bb28fd
9f1c50c
 
3bb28fd
 
 
 
9f1c50c
3bb28fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f1c50c
 
 
 
 
 
3bb28fd
 
 
 
 
 
 
 
 
 
 
 
 
 
9f1c50c
 
 
3bb28fd
 
 
 
 
9f1c50c
3bb28fd
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import gradio as gr
from gradio import Server
import spaces
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

# ─────────────────────────────────────────────
# 1. MODEL SETUP
# ─────────────────────────────────────────────
MODEL_ID = "huihui-ai/Huihui-Qwen3.5-9B-abliterated"

print(f"Loading tokenizer: {MODEL_ID}")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)

print(f"Loading model: {MODEL_ID}")
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.float16,
    device_map="auto",       # ZeroGPU manages CUDA device
    trust_remote_code=True,  # Needed for Qwen-based custom archs
)
model.eval()

# ─────────────────────────────────────────────
# 1b. THINKING ENFORCEMENT
#
#  This model isn't guaranteed to emit <think>...</think> on its own
#  (especially as an abliterated finetune). To make it 100% reliable:
#
#    1. Try passing enable_thinking=True to apply_chat_template
#       (native support on Qwen3-style templates, if present).
#    2. ALWAYS force-append "<think>\n" onto the templated prompt
#       so generation is physically forced to begin inside a think
#       block, regardless of whether enable_thinking worked.
#    3. Append a short mandatory instruction onto whatever system
#       prompt is passed in, telling the model to close the tag.
#    4. Manually re-prepend "<think>\n" onto the decoded output,
#       since it was part of the forced prompt and gets stripped
#       out by skip_prompt / prompt-slicing.
# ─────────────────────────────────────────────
THINK_INSTRUCTION = (
    "\n\nAlways reason through the problem step by step inside <think> "
    "and </think> tags first. After the closing </think> tag, give your "
    "final answer. Never skip the opening or closing think tags."
)


def build_prompt(prompt: str, system_prompt: str) -> str:
    full_system = (system_prompt or "You are a helpful assistant.") + THINK_INSTRUCTION

    messages = [
        {"role": "system", "content": full_system},
        {"role": "user", "content": prompt},
    ]

    try:
        # Native Qwen3-style thinking toggle, if the tokenizer supports it
        text = tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
            enable_thinking=True,
        )
    except TypeError:
        # Tokenizer/template doesn't accept enable_thinking β€” fall back
        text = tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
        )

    # Force the assistant turn to start inside a <think> block,
    # guaranteeing the tag appears no matter what the model would
    # have done on its own.
    if not text.rstrip().endswith("<think>"):
        text = text + "<think>\n"

    return text


# ─────────────────────────────────────────────
# 2. INFERENCE FUNCTION (ZeroGPU decorated)
# ─────────────────────────────────────────────
@spaces.GPU(duration=120)
def generate(
    prompt: str,
    system_prompt: str = "You are a helpful assistant.",
    max_new_tokens: int = 512,
    temperature: float = 0.7,
    top_p: float = 0.9,
    do_sample: bool = True,
) -> str:
    """
    Generate a text response from the model.
    Response is guaranteed to start with a <think> block.
    """
    text = build_prompt(prompt, system_prompt)
    inputs = tokenizer(text, return_tensors="pt").to(model.device)

    with torch.no_grad():
        output_ids = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            temperature=temperature,
            top_p=top_p,
            do_sample=do_sample,
            pad_token_id=tokenizer.eos_token_id,
        )

    new_tokens = output_ids[0][inputs["input_ids"].shape[1]:]
    response = tokenizer.decode(new_tokens, skip_special_tokens=True)

    # Re-add the <think> tag we force-injected into the prompt β€”
    # it was stripped out because it's technically part of the input.
    response = "<think>\n" + response
    return response


# ─────────────────────────────────────────────
# 3. STREAMING INFERENCE (SSE / token-by-token)
# ─────────────────────────────────────────────
@spaces.GPU(duration=120)
def generate_stream(
    prompt: str,
    system_prompt: str = "You are a helpful assistant.",
    max_new_tokens: int = 512,
    temperature: float = 0.7,
    top_p: float = 0.9,
):
    """
    Stream a text response token-by-token via SSE.
    Guaranteed to start with a <think> block.
    """
    from transformers import TextIteratorStreamer
    from threading import Thread

    text = build_prompt(prompt, system_prompt)
    inputs = tokenizer(text, return_tensors="pt").to(model.device)

    streamer = TextIteratorStreamer(
        tokenizer, skip_prompt=True, skip_special_tokens=True
    )

    gen_kwargs = dict(
        **inputs,
        max_new_tokens=max_new_tokens,
        temperature=temperature,
        top_p=top_p,
        do_sample=True,
        streamer=streamer,
        pad_token_id=tokenizer.eos_token_id,
    )

    thread = Thread(target=model.generate, kwargs=gen_kwargs)
    thread.start()

    # Re-inject the forced <think> prefix as the very first chunk,
    # since it was part of the prompt and streamer.skip_prompt=True
    # will not emit it on its own.
    partial = "<think>\n"
    yield partial

    for new_text in streamer:
        partial += new_text
        yield partial


# ─────────────────────────────────────────────
# 4. gr.Server β€” REST API + OPTIONAL SWAGGER UI
# ─────────────────────────────────────────────
app = Server(
    title="Mira-1-Large API",
    summary="ZeroGPU-backed REST API for Smilyai-labs/Mira-1-large (Qwen arch)",
    version="1.0.0",
)

app.api(generate,        name="generate")
app.api(generate_stream, name="generate_stream")


@app.get("/health")
def health():
    return {"status": "ok", "model": MODEL_ID}


# ─────────────────────────────────────────────
# 5. LAUNCH
# ─────────────────────────────────────────────
app.launch()