char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
d73c1d6b2
char/aliceai-t5-moe
1import json 2import socket 3import threading 4import time 5from urllib .parse import quote 6import pytest 7from utils import * 8 9server :ServerProcess 10 11# a model name with slashes exercises the query string routing of the stream routes: the id 12# cannot travel as a path param because the decoded slash would split it before capture 13MODEL = "ggml-org/tinygemma3-GGUF:Q8_0" 14STREAM_ID = f"conv-stream-test:: { MODEL } " 15QS = "conv_id=" + quote (STREAM_ID ,safe = "" ) 16 17 18@ pytest . fixture ( autouse = True ) 19def create_server (): 20global server 21server = ServerPreset .router () 22 23 24def test_stream_resume_and_stop_with_slashed_model_name (): 25global server 26server .start () 27 28content = "" 29for data in server .make_stream_request ("POST" ,"/chat/completions" ,data = { 30"model" :MODEL , 31"stream" :True , 32"max_tokens" :16 , 33"messages" : [{"role" :"user" ,"content" :"hello" }], 34 },headers = {"X-Conversation-Id" :STREAM_ID }): 35if data ["choices" ]: 36content += data ["choices" ][0 ]["delta" ].get ("content" )or "" 37assert len (content )> 0 38 39# the finished session replays from the beginning through the router 40res = server .make_request ("GET" ,f"/v1/stream? { QS } &from=0" ) 41assert res .status_code == 200 42assert "data: " in str (res .body ) 43 44# the explicit stop reaches the owning child and evicts the session 45res = server .make_request ("DELETE" ,f"/v1/stream? { QS } " ) 46assert res .status_code == 204 47res = server .make_request ("GET" ,f"/v1/stream? { QS } &from=0" ) 48assert res .status_code == 404 49 50 51def test_stream_stop_during_model_load (): 52global server 53server .start () 54 55thread_error :list [ServerError ]= [] 56thread_done = threading .Event () 57 58def fire_post (): 59try : 60for _ in server .make_stream_request ("POST" ,"/chat/completions" ,data = { 61"model" :MODEL , 62"stream" :True , 63"max_tokens" :512 , 64"messages" : [{"role" :"user" ,"content" :"Count from 1 to 1000." }], 65 },headers = {"X-Conversation-Id" :STREAM_ID }): 66pass 67except ServerError as e : 68thread_error .append (e ) 69finally : 70thread_done .set () 71 72t = threading .Thread (target = fire_post ) 73t .start () 74 75# catch the autoload window, tiny models load fast so poll aggressively 76saw_loading = False 77deadline = time .time ()+ 5.0 78while time .time ()< deadline and not thread_done .is_set (): 79res = server .make_request ("GET" ,"/models" ) 80status = next (m ["status" ]["value" ]for m in res .body ["data" ]if m ["id" ]== MODEL ) 81if status == "loading" : 82saw_loading = True 83break 84time .sleep (0.002 ) 85if not saw_loading : 86t .join () 87pytest .skip ("load window too short to be observed on this machine" )# ty: ignore[too-many-positional-arguments] 88 89# a stop during the load cancels the parked request instead of leaving an orphan 90res = server .make_request ("DELETE" ,f"/v1/stream? { QS } " ) 91assert res .status_code == 204 92assert thread_done .wait (timeout = 60 ) 93t .join () 94assert len (thread_error )== 1 95assert thread_error [0 ].code == 400 96assert "cancelled" in json .dumps (thread_error [0 ].body ) 97res = server .make_request ("GET" ,f"/v1/stream? { QS } &from=0" ) 98assert res .status_code == 404 99 100 101def test_stream_resumes_after_reload_during_model_load (): 102global server 103server .start () 104 105# raw socket client so the connection can be dropped mid load like a page reload 106body = json .dumps ({ 107"model" :MODEL , 108"stream" :True , 109"max_tokens" :16 , 110"messages" : [{"role" :"user" ,"content" :"hello" }], 111 }) 112request = ( 113f"POST /v1/chat/completions HTTP/1.1\r\n" 114f"Host: { server . server_host } : { server . server_port } \r\n" 115f"Content-Type: application/json\r\n" 116f"X-Conversation-Id: { STREAM_ID } \r\n" 117f"Content-Length: { len ( body ) } \r\n" 118f"Connection: close\r\n\r\n { body } " 119 ) 120sock = socket .create_connection ((server .server_host ,server .server_port )) 121sock .sendall (request .encode ()) 122 123# drop the client while the model loads, poll aggressively to catch the window 124saw_loading = False 125saw_503 = False 126deadline = time .time ()+ 5.0 127while time .time ()< deadline : 128res = server .make_request ("GET" ,"/models" ) 129status = next (m ["status" ]["value" ]for m in res .body ["data" ]if m ["id" ]== MODEL ) 130if status == "loading" : 131saw_loading = True 132break 133if status == "loaded" : 134break 135time .sleep (0.002 ) 136sock .close () 137if not saw_loading : 138pytest .skip ("load window too short to be observed on this machine" )# ty: ignore[too-many-positional-arguments] 139 140# while the model loads the resume route answers retry later, then the session appears, 141# receives the whole generation despite the dead client, and replays from the beginning 142deadline = time .time ()+ 60.0 143replay = None 144while time .time ()< deadline : 145res = server .make_request ("GET" ,f"/v1/stream? { QS } &from=0" ) 146if res .status_code == 503 : 147saw_503 = True 148elif res .status_code == 200 and "data: " in str (res .body ): 149replay = res 150break 151time .sleep (0.1 ) 152assert saw_503 ,"resume during the load did not answer 503" 153assert replay is not None ,"session never became resumable after the client disconnect"