File size: 7,464 Bytes
8b8e98d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""test_openai_api.py — openai_api.py 的 mock 全端点联调 (本地可跑, 无需 GPU/transformers)
覆盖: /health /v1/models /v1/chat/completions (非流式 bare / think_off / 模型名后缀 /
      raw_content / 流式SSE含usage) /v1/completions / 错误分支 (401/400)
"""
import json
import subprocess
import sys
import time
import urllib.request
import urllib.error

PORT = 8399
BASE = f"http://127.0.0.1:{PORT}"
PY = sys.executable


def req(method, path, body=None, headers=None, timeout=15):
    data = json.dumps(body).encode() if body is not None else None
    r = urllib.request.Request(BASE + path, data=data, method=method,
                               headers={"Content-Type": "application/json",
                                        **(headers or {})})
    try:
        with urllib.request.urlopen(r, timeout=timeout) as resp:
            return resp.status, resp.read().decode()
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode()


def raw_stream(body):
    r = urllib.request.Request(BASE + "/v1/chat/completions",
                               data=json.dumps(body).encode(), method="POST",
                               headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(r, timeout=20) as resp:
        return resp.read().decode()


fails = []


def check(name, cond, detail=""):
    print(("PASS " if cond else "FAIL ") + name + ("" if cond else f"  <- {detail}"))
    if not cond:
        fails.append(name)


def main():
    proc = subprocess.Popen([PY, "/home/z/my-project/scripts/openai_api.py",
                             "--mock", "--port", str(PORT)],
                            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    try:
        for _ in range(40):
            try:
                s, b = req("GET", "/health")
                if s == 200 and json.loads(b).get("loaded"):
                    break
            except Exception:
                pass
            time.sleep(0.25)
        else:
            print("server not up")
            sys.exit(1)

        s, b = req("GET", "/health")
        h = json.loads(b)
        check("health.loaded", s == 200 and h["mock"] is True and h["loaded"] is True, b[:120])

        s, b = req("GET", "/v1/models")
        m = json.loads(b)
        check("models.list", s == 200 and m["object"] == "list"
              and any(x["id"] == "samai-2b" for x in m["data"]), b[:120])

        # bare 协议 (默认): content 剥 think + reasoning_content + raw
        s, b = req("POST", "/v1/chat/completions", {
            "model": "samai-2b",
            "messages": [{"role": "user", "content": "你好"}],
            "max_tokens": 100})
        d = json.loads(b)
        ch = d["choices"][0]
        check("chat.bare.status", s == 200 and d["object"] == "chat.completion", b[:200])
        check("chat.bare.content_stripped", "<think>" not in ch["message"]["content"]
              and ch["message"]["content"].startswith("[mock]"), ch["message"]["content"][:80])
        check("chat.bare.reasoning", "reasoning_content" in ch["message"]
              and "mock" in ch["message"]["reasoning_content"], str(ch["message"])[:150])
        check("chat.bare.raw_kept", ch["message"]["raw"].startswith("<think>"), "")
        check("chat.bare.usage", d["usage"]["total_tokens"] ==
              d["usage"]["prompt_tokens"] + d["usage"]["completion_tokens"], str(d["usage"]))
        check("chat.bare.finish", ch["finish_reason"] == "stop", ch["finish_reason"])
        check("chat.bare.ponder", isinstance(d.get("ponder"), dict)
              and "steps_mean" in d["ponder"], str(d.get("ponder")))
        check("chat.bare.ids", d["id"].startswith("chatcmpl-") and d["model"] == "samai-2b", d["id"])

        # think_off via enable_thinking=false
        s, b = req("POST", "/v1/chat/completions", {
            "model": "samai-2b", "enable_thinking": False,
            "messages": [{"role": "user", "content": "12x8?"}]})
        d = json.loads(b)
        check("chat.thinkoff.no_think", "<think>" not in d["choices"][0]["message"]["content"]
              and "reasoning_content" not in d["choices"][0]["message"], b[:200])

        # think_off via chat_template_kwargs
        s, b = req("POST", "/v1/chat/completions", {
            "model": "samai-2b", "chat_template_kwargs": {"enable_thinking": False},
            "messages": [{"role": "user", "content": "12x8?"}]})
        d = json.loads(b)
        check("chat.ctk.thinkoff", "<think>" not in d["choices"][0]["message"]["content"], b[:150])

        # 模型名后缀 -think-off
        s, b = req("POST", "/v1/chat/completions", {
            "model": "samai-2b-think-off",
            "messages": [{"role": "user", "content": "hi"}]})
        d = json.loads(b)
        check("chat.suffix.thinkoff", "<think>" not in d["choices"][0]["message"]["content"]
              and d["model"] == "samai-2b", b[:150])

        # raw_content=true → content=原始全文
        s, b = req("POST", "/v1/chat/completions", {
            "model": "samai-2b", "raw_content": True,
            "messages": [{"role": "user", "content": "hi"}]})
        d = json.loads(b)
        check("chat.rawcontent", d["choices"][0]["message"]["content"].startswith("<think>"), b[:150])

        # 流式 SSE
        body = {"model": "samai-2b",
                "messages": [{"role": "user", "content": "流式测试"}],
                "stream": True, "stream_options": {"include_usage": True}}
        sse = raw_stream(body)
        lines = [l for l in sse.split("\n\n") if l.startswith("data: ")]
        objs = [json.loads(l[6:]) for l in lines if l[6:] != "[DONE]"]
        ids = {o["id"] for o in objs if o["choices"]}
        contents = "".join((o["choices"][0]["delta"].get("content") or "")
                           for o in objs if o["choices"])
        finishes = [o["choices"][0]["finish_reason"] for o in objs
                    if o["choices"] and o["choices"][0]["finish_reason"]]
        has_usage = any(o.get("usage") for o in objs)
        has_done = "[DONE]" in sse
        check("stream.ids_consistent", len(ids) == 1, str(ids))
        check("stream.content_no_think", "<think>" not in contents
              and contents.startswith("[mock]"), contents[:80])
        check("stream.reasoning_streamed", any((o["choices"][0]["delta"].get("reasoning_content"))
                                               for o in objs if o["choices"]), "")
        check("stream.finish_and_done", finishes == ["stop"] and has_done, f"{finishes} done={has_done}")
        check("stream.usage", has_usage, "")

        # /v1/completions
        s, b = req("POST", "/v1/completions", {"model": "samai-2b", "prompt": "续写测试", "max_tokens": 32})
        d = json.loads(b)
        check("completions.basic", s == 200 and "text" in d["choices"][0]
              and d["usage"]["completion_tokens"] > 0, b[:150])

        # 错误分支
        s, b = req("POST", "/v1/chat/completions", {"messages": []})
        check("err.empty_messages", s == 400 and "error" in json.loads(b), b[:120])
        s, b = req("POST", "/v1/chat/completions", {"model": "x"})
        check("err.bad_json_shape", s == 400, b[:120])

        print("\n%s  (fails=%d)" % ("ALL_PASS" if not fails else "FAILED", len(fails)))
        sys.exit(0 if not fails else 1)
    finally:
        proc.terminate()


if __name__ == "__main__":
    main()