v0.7.5: arrow keys, image request fix, deepseek-flash vision, musl CI

TUI keys: register both CSI and SS3 forms of the cursor/Home/End keys and
the xterm modifier sequences when terminfo lacks them, so an arrow sent in
the "other" form no longer turns into Esc + a typed letter. Up/Down move by
visual (soft-wrapped) row, and the draft survives a trip through history.
scripts/tui_keys drives the TUI in a pty to check all of this.

Images: text inside a multimodal content array is now a {"type":"text"}
part; DeepSeek rejected the bare string with HTTP 422. deepseek-flash
(and its deepseek-chat / deepseek-reasoner aliases) read images, so the
registry marks them vision; deepseek-v4-pro stays text only.

CI: models.c lost _DEFAULT_SOURCE in 0.7.0 and gmtime_r broke the musl
build; tests sleep with poll() instead of usleep for the same reason.
This commit is contained in:
WHC
2026-10-08 22:35:34 +08:00
parent 32b4afd772
commit c7b32c3fd6
13 changed files with 646 additions and 49 deletions
+43
View File
@@ -0,0 +1,43 @@
import json, sys, threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
LOG = []
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_GET(self):
body = json.dumps({"data": [{"id": "deepseek-flash"}, {"id": "deepseek-v4-pro"}]}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self):
n = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(n)
try:
LOG.append(json.loads(raw))
except Exception:
LOG.append({"_raw": raw[:200].decode("latin1")})
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.end_headers()
def ev(o):
self.wfile.write(b"data: " + json.dumps(o).encode() + b"\n\n")
self.wfile.flush()
base = {"id": "x", "object": "chat.completion.chunk", "model": "deepseek-flash"}
ev({**base, "choices": [{"index": 0, "delta": {"role": "assistant", "content": "reply-%d" % len(LOG)}}]})
ev({**base, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}})
self.wfile.write(b"data: [DONE]\n\n")
self.wfile.flush()
def start():
s = ThreadingHTTPServer(("127.0.0.1", 0), H)
threading.Thread(target=s.serve_forever, daemon=True).start()
return s, "http://127.0.0.1:%d/v1" % s.server_address[1]