Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7b32c3fd6 |
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#define CST_VERSION_MAJOR 0
|
#define CST_VERSION_MAJOR 0
|
||||||
#define CST_VERSION_MINOR 7
|
#define CST_VERSION_MINOR 7
|
||||||
#define CST_VERSION_PATCH 1
|
#define CST_VERSION_PATCH 5
|
||||||
|
|
||||||
#define CST_VERSION_STR_(x) #x
|
#define CST_VERSION_STR_(x) #x
|
||||||
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import os
|
||||||
|
import sys, time
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from harness import Term
|
||||||
|
import fakeapi
|
||||||
|
from keys import input_text, styled, check, FAILS, base
|
||||||
|
|
||||||
|
for term in ("xterm-256color", "screen-256color"):
|
||||||
|
t = Term(args=("-b", base, "-k", "sk-test"), term=term)
|
||||||
|
t.send("some text")
|
||||||
|
t.send("\x1b", 0.4)
|
||||||
|
check(term + " lone esc clears input", input_text(t) == [], t)
|
||||||
|
t.send("alpha beta")
|
||||||
|
t.send("\x1bb") # alt+b word left
|
||||||
|
t.send("X")
|
||||||
|
check(term + " alt+b word left", "alpha Xbeta" in input_text(t), t)
|
||||||
|
t.send("\x1b\r") # alt+enter newline
|
||||||
|
t.send("Y")
|
||||||
|
check(term + " alt+enter newline", input_text(t)[-2:] == ["alpha X", "Ybeta"], t)
|
||||||
|
t.send("\x03", 0.2)
|
||||||
|
# picker with CSI form
|
||||||
|
t.send("/model\r", 0.8)
|
||||||
|
s0 = styled(t)
|
||||||
|
t.arrow("down", "csi")
|
||||||
|
s1 = styled(t)
|
||||||
|
t.arrow("up", "csi")
|
||||||
|
s2 = styled(t)
|
||||||
|
check(term + " picker csi down", s0 != s1, t)
|
||||||
|
check(term + " picker csi up", s0 == s2, t)
|
||||||
|
t.send("\x1b", 0.4)
|
||||||
|
# home/end variants
|
||||||
|
t.send("middle")
|
||||||
|
for seq in ("\x1b[H", "\x1bOH", "\x1b[1~", "\x1b[7~"):
|
||||||
|
t.send("\x1b[F" + seq)
|
||||||
|
t.send("<")
|
||||||
|
t.send("\x08" if False else "")
|
||||||
|
check(term + " home variants", input_text(t)[-1:] == ["<<<<middle"], t)
|
||||||
|
for seq in ("\x1b[F", "\x1bOF", "\x1b[4~", "\x1b[8~"):
|
||||||
|
t.send("\x1b[H" + seq)
|
||||||
|
t.send(">")
|
||||||
|
check(term + " end variants", input_text(t)[-1:] == ["<<<<middle>>>>"], t)
|
||||||
|
t.close()
|
||||||
|
print("FAILED:", len(FAILS))
|
||||||
|
sys.exit(min(len(FAILS), 100))
|
||||||
@@ -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]
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""Drive the cst-agent TUI in a pty and read the screen back (pyte).
|
||||||
|
|
||||||
|
Run from the repo root after `make`:
|
||||||
|
nix-shell -p python3Packages.pyte --run "python3 scripts/tui_keys/keys.py"
|
||||||
|
nix-shell -p python3Packages.pyte --run "python3 scripts/tui_keys/esc.py"
|
||||||
|
(or `pip install pyte`). A local fake OpenAI-compatible server answers the
|
||||||
|
model calls, so no API key is needed. Exit status is the failure count.
|
||||||
|
"""
|
||||||
|
import os, pty, sys, time, select, fcntl, termios, struct, signal, shutil, tempfile
|
||||||
|
import pyte
|
||||||
|
|
||||||
|
BIN = os.environ.get("CST_BIN") or os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "..", "cst-agent")
|
||||||
|
|
||||||
|
|
||||||
|
class Term:
|
||||||
|
def __init__(self, cols=120, rows=36, kitty=False, env=None, args=(), home=None,
|
||||||
|
ws=None, term="xterm-256color"):
|
||||||
|
self.cols, self.rows, self.kitty = cols, rows, kitty
|
||||||
|
self.screen = pyte.Screen(cols, rows)
|
||||||
|
self.stream = pyte.ByteStream(self.screen)
|
||||||
|
self.home = home or tempfile.mkdtemp(prefix="cstk_home_")
|
||||||
|
self.ws = ws or tempfile.mkdtemp(prefix="cstk_ws_")
|
||||||
|
e = dict(os.environ)
|
||||||
|
e.update({"TERM": term, "HOME": self.home,
|
||||||
|
"XDG_CONFIG_HOME": os.path.join(self.home, ".config"),
|
||||||
|
"LANG": "C.UTF-8", "LC_ALL": "C.UTF-8",
|
||||||
|
"CST_PROVIDER": "deepseek", "DEEPSEEK_API_KEY": "sk-dummy-test",
|
||||||
|
"CST_METRICS": "0"})
|
||||||
|
e.pop("NO_COLOR", None)
|
||||||
|
if env:
|
||||||
|
e.update(env)
|
||||||
|
self.raw = b""
|
||||||
|
pid, fd = pty.fork()
|
||||||
|
if pid == 0:
|
||||||
|
os.chdir(self.ws)
|
||||||
|
os.execve(BIN, [BIN, "-N", *args], e)
|
||||||
|
self.pid, self.fd = pid, fd
|
||||||
|
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
|
||||||
|
self.pump(1.5)
|
||||||
|
|
||||||
|
def pump(self, t=0.25):
|
||||||
|
end = time.time() + t
|
||||||
|
while True:
|
||||||
|
left = end - time.time()
|
||||||
|
if left <= 0:
|
||||||
|
break
|
||||||
|
r, _, _ = select.select([self.fd], [], [], left)
|
||||||
|
if not r:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
d = os.read(self.fd, 65536)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
self.raw += d
|
||||||
|
if self.kitty and b"\x1b[?u" in d:
|
||||||
|
os.write(self.fd, b"\x1b[?0u")
|
||||||
|
self.stream.feed(d)
|
||||||
|
|
||||||
|
def send(self, b, t=0.15):
|
||||||
|
if isinstance(b, str):
|
||||||
|
b = b.encode()
|
||||||
|
os.write(self.fd, b)
|
||||||
|
self.pump(t)
|
||||||
|
|
||||||
|
def appcursor(self):
|
||||||
|
return (1 << 5) in self.screen.mode # pyte DECCKM
|
||||||
|
|
||||||
|
def arrow(self, d, form=None, mod=None, t=0.15):
|
||||||
|
c = {"up": "A", "down": "B", "right": "C", "left": "D"}[d]
|
||||||
|
if mod:
|
||||||
|
seq = "\x1b[1;%d%s" % (mod, c)
|
||||||
|
else:
|
||||||
|
f = form or ("ss3" if self.appcursor() else "csi")
|
||||||
|
seq = ("\x1bO" if f == "ss3" else "\x1b[") + c
|
||||||
|
self.send(seq, t)
|
||||||
|
|
||||||
|
def lines(self):
|
||||||
|
return [l.rstrip() for l in self.screen.display]
|
||||||
|
|
||||||
|
def find(self, s):
|
||||||
|
return [(i, l) for i, l in enumerate(self.lines()) if s in l]
|
||||||
|
|
||||||
|
def cursor(self):
|
||||||
|
return self.screen.cursor.y, self.screen.cursor.x
|
||||||
|
|
||||||
|
def dump(self, title=""):
|
||||||
|
print("=" * 20, title, "cursor", self.cursor())
|
||||||
|
for i, l in enumerate(self.lines()):
|
||||||
|
print("%2d|%s" % (i, l))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try:
|
||||||
|
os.kill(self.pid, signal.SIGTERM)
|
||||||
|
os.waitpid(self.pid, 0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import os
|
||||||
|
import sys, time
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from harness import Term
|
||||||
|
import fakeapi
|
||||||
|
|
||||||
|
srv, base = fakeapi.start()
|
||||||
|
FAILS = []
|
||||||
|
VERBOSE = "-v" in sys.argv
|
||||||
|
|
||||||
|
|
||||||
|
def check(name, cond, t=None):
|
||||||
|
print(("PASS " if cond else "FAIL ") + name)
|
||||||
|
if not cond:
|
||||||
|
FAILS.append(name)
|
||||||
|
if t:
|
||||||
|
t.dump(name)
|
||||||
|
|
||||||
|
|
||||||
|
def input_text(t):
|
||||||
|
"""Input box rows: the bar rows right above the mode line."""
|
||||||
|
L = t.lines()
|
||||||
|
mode = [i for i, l in enumerate(L) if l.lstrip().startswith("┃ ") and
|
||||||
|
("Build ·" in l or "Plan ·" in l)]
|
||||||
|
if not mode:
|
||||||
|
return []
|
||||||
|
rows = []
|
||||||
|
i = mode[-1] - 1
|
||||||
|
while i >= 0:
|
||||||
|
s = L[i].lstrip()
|
||||||
|
if not s.startswith("┃"):
|
||||||
|
break
|
||||||
|
body = s[2:].split(" ")[0].rstrip() if s.startswith("┃ ") else ""
|
||||||
|
if body == "" and rows:
|
||||||
|
break
|
||||||
|
if body:
|
||||||
|
rows.append(body)
|
||||||
|
i -= 1
|
||||||
|
rows.reverse()
|
||||||
|
if rows and (rows[0].startswith("Ask anything") or rows[0].startswith("Enter queues")):
|
||||||
|
return []
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def styled(t):
|
||||||
|
"""Screen text plus the reverse/bg attributes, to see highlight moves."""
|
||||||
|
out = []
|
||||||
|
for y in range(t.rows):
|
||||||
|
row = t.screen.buffer[y]
|
||||||
|
out.append("".join("%s%s%s" % (row[x].data, int(row[x].reverse), row[x].bg[:3])
|
||||||
|
for x in range(t.cols)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def clear(t):
|
||||||
|
if [r for r in input_text(t) if r]:
|
||||||
|
t.send("\x03", 0.2) # ctrl+c clears a non-empty input (twice on empty quits)
|
||||||
|
|
||||||
|
|
||||||
|
def new(kitty=False, **kw):
|
||||||
|
t = Term(kitty=kitty, args=("-b", base, "-k", "sk-test", "-m", "deepseek-flash"), **kw)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def basic(t, form, tag):
|
||||||
|
clear(t)
|
||||||
|
t.send("hello world")
|
||||||
|
for _ in range(5):
|
||||||
|
t.arrow("left", form)
|
||||||
|
t.send("X")
|
||||||
|
check(f"{tag} left x5 insert", "hello Xworld" in input_text(t), t)
|
||||||
|
t.arrow("right", form); t.arrow("right", form)
|
||||||
|
t.send("Y")
|
||||||
|
check(f"{tag} right x2 insert", "hello XwoYrld" in input_text(t), t)
|
||||||
|
# boundaries
|
||||||
|
for _ in range(30):
|
||||||
|
t.arrow("left", form, t=0.02)
|
||||||
|
t.pump(0.2)
|
||||||
|
t.send("<")
|
||||||
|
check(f"{tag} left past start", "<hello XwoYrld" in input_text(t), t)
|
||||||
|
for _ in range(30):
|
||||||
|
t.arrow("right", form, t=0.02)
|
||||||
|
t.pump(0.2)
|
||||||
|
t.send(">")
|
||||||
|
check(f"{tag} right past end", "<hello XwoYrld>" in input_text(t), t)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def cjk(t):
|
||||||
|
clear(t)
|
||||||
|
t.send("你好世界")
|
||||||
|
y, x_end = t.cursor()
|
||||||
|
t.arrow("left"); t.arrow("left")
|
||||||
|
y2, x2 = t.cursor()
|
||||||
|
check("cjk cursor moves 2 cells per char", x_end - x2 == 4, t)
|
||||||
|
t.send("A")
|
||||||
|
check("cjk insert between wide chars", "你好A世界" in input_text(t), t)
|
||||||
|
t.arrow("right")
|
||||||
|
t.send("B")
|
||||||
|
check("cjk right over wide char", "你好A世B界" in input_text(t), t)
|
||||||
|
clear(t)
|
||||||
|
t.send("a😀b")
|
||||||
|
t.arrow("left"); t.arrow("left")
|
||||||
|
t.send("Z")
|
||||||
|
check("emoji left", "aZ😀b" in input_text(t), t)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def multiline(t):
|
||||||
|
clear(t)
|
||||||
|
t.send("line one is long")
|
||||||
|
t.send("\x0e") # ctrl+n newline
|
||||||
|
t.send("l2")
|
||||||
|
t.send("\x0e")
|
||||||
|
t.send("third line here")
|
||||||
|
rows = input_text(t)
|
||||||
|
check("multiline 3 rows", rows[-3:] == ["line one is long", "l2", "third line here"], t)
|
||||||
|
for _ in range(4):
|
||||||
|
t.arrow("left") # cursor before "here"
|
||||||
|
t.arrow("up") # col 11 -> clamp to end of "l2"
|
||||||
|
t.send("U")
|
||||||
|
rows = input_text(t)
|
||||||
|
check("up clamps column to short line", rows[-2] == "l2U", t)
|
||||||
|
t.arrow("up") # col 3 on line one
|
||||||
|
t.send("V")
|
||||||
|
rows = input_text(t)
|
||||||
|
check("up keeps column", rows[-3] == "linVe one is long", t)
|
||||||
|
t.arrow("down"); t.arrow("down")
|
||||||
|
t.send("W")
|
||||||
|
rows = input_text(t)
|
||||||
|
check("down twice to last line", "W" in rows[-1], t)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def history(t):
|
||||||
|
clear(t)
|
||||||
|
t.send("first prompt\r", 1.5)
|
||||||
|
t.send("second prompt\r", 1.5)
|
||||||
|
t.arrow("up")
|
||||||
|
check("hist up -> newest", input_text(t)[-1:] == ["second prompt"], t)
|
||||||
|
t.arrow("up")
|
||||||
|
check("hist up -> older", input_text(t)[-1:] == ["first prompt"], t)
|
||||||
|
t.arrow("up")
|
||||||
|
check("hist up at oldest stays", input_text(t)[-1:] == ["first prompt"], t)
|
||||||
|
t.arrow("down")
|
||||||
|
check("hist down -> newer", input_text(t)[-1:] == ["second prompt"], t)
|
||||||
|
t.arrow("down")
|
||||||
|
check("hist down past newest -> empty", input_text(t) in ([], [""]), t)
|
||||||
|
# draft survives a trip through history
|
||||||
|
t.send("my draft")
|
||||||
|
t.arrow("up")
|
||||||
|
t.arrow("down")
|
||||||
|
check("draft restored after up/down", input_text(t)[-1:] == ["my draft"], t)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def wrapped(t):
|
||||||
|
clear(t)
|
||||||
|
long = " ".join("w%02d" % i for i in range(40)) # ~160 chars, wraps
|
||||||
|
t.send(long, 0.4)
|
||||||
|
nrows = len(input_text(t))
|
||||||
|
t.arrow("up")
|
||||||
|
txt = "".join(input_text(t))
|
||||||
|
check("up on wrapped line keeps the text", "w39" in txt and "w00" in txt, t)
|
||||||
|
t.send("Q")
|
||||||
|
rows = input_text(t)
|
||||||
|
check("up on wrapped line moves one visual row", nrows >= 2 and "Q" in "".join(rows[:-1]), t)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def completion(t):
|
||||||
|
clear(t)
|
||||||
|
t.send("/", 0.3)
|
||||||
|
before = styled(t)
|
||||||
|
t.arrow("down"); t.arrow("down")
|
||||||
|
mid = styled(t)
|
||||||
|
t.arrow("up")
|
||||||
|
after = styled(t)
|
||||||
|
check("completion popup reacts to down", before != mid, t)
|
||||||
|
check("completion up differs from down-down", mid != after, t)
|
||||||
|
check("completion arrows do not touch input", input_text(t)[-1:] == ["/"], t)
|
||||||
|
t.send("\x1b", 0.3)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def modifiers(t):
|
||||||
|
clear(t)
|
||||||
|
t.send("alpha beta gamma")
|
||||||
|
t.arrow("left", mod=5) # ctrl+left -> word left
|
||||||
|
t.send("X")
|
||||||
|
check("ctrl+left word", "alpha beta Xgamma" in input_text(t), t)
|
||||||
|
t.arrow("left", mod=3) # alt+left
|
||||||
|
t.arrow("left", mod=3)
|
||||||
|
t.send("Y")
|
||||||
|
check("alt+left word", "alpha Ybeta Xgamma" in input_text(t), t)
|
||||||
|
t.arrow("right", mod=5)
|
||||||
|
t.send("Z")
|
||||||
|
check("ctrl+right word", "alpha YbetaZ Xgamma" in input_text(t), t)
|
||||||
|
t.arrow("left", mod=2) # shift+left: should at least not insert junk
|
||||||
|
t.send("S")
|
||||||
|
txt = "".join(input_text(t))
|
||||||
|
check("shift+left no junk", "[" not in txt and ";" not in txt and "1;2" not in txt, t)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def split_and_burst(t, form):
|
||||||
|
clear(t)
|
||||||
|
t.send("abcdef")
|
||||||
|
ss = "\x1bO" if form == "ss3" else "\x1b["
|
||||||
|
# escape sequence split across writes (slow link)
|
||||||
|
import os
|
||||||
|
os.write(t.fd, ss[:1].encode()); time.sleep(0.008)
|
||||||
|
os.write(t.fd, (ss[1:] + "D").encode()); t.pump(0.2)
|
||||||
|
t.send("1")
|
||||||
|
check(f"split {form} seq", "abcde1f" in input_text(t), t)
|
||||||
|
# key-repeat burst in one write
|
||||||
|
t.send((ss + "D") * 4, 0.3)
|
||||||
|
t.send("2")
|
||||||
|
check(f"burst {form} x4", "ab2cde1f" in input_text(t), t)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def picker(t):
|
||||||
|
clear(t)
|
||||||
|
t.send("/model\r", 0.8)
|
||||||
|
s0 = styled(t)
|
||||||
|
t.arrow("down")
|
||||||
|
s1 = styled(t)
|
||||||
|
t.arrow("up")
|
||||||
|
s2 = styled(t)
|
||||||
|
check("picker down changes selection", s0 != s1, t)
|
||||||
|
check("picker up returns", s0 == s2, t)
|
||||||
|
t.send("\x1b", 0.4)
|
||||||
|
clear(t)
|
||||||
|
|
||||||
|
|
||||||
|
def run(kitty, tag):
|
||||||
|
print("#####", tag)
|
||||||
|
t = new(kitty=kitty)
|
||||||
|
try:
|
||||||
|
basic(t, None, tag + " native")
|
||||||
|
basic(t, "csi" if t.appcursor() else "ss3", tag + " other-form")
|
||||||
|
cjk(t)
|
||||||
|
multiline(t)
|
||||||
|
modifiers(t)
|
||||||
|
split_and_burst(t, "ss3")
|
||||||
|
split_and_burst(t, "csi")
|
||||||
|
completion(t)
|
||||||
|
wrapped(t)
|
||||||
|
history(t)
|
||||||
|
picker(t)
|
||||||
|
finally:
|
||||||
|
t.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run(False, "xterm")
|
||||||
|
run(True, "kitty")
|
||||||
|
for term in ("screen-256color", "tmux-256color", "linux", "vt220", "xterm"):
|
||||||
|
print("#####", term)
|
||||||
|
t = Term(args=("-b", base, "-k", "sk-test"), term=term)
|
||||||
|
try:
|
||||||
|
basic(t, "ss3", term + " ss3")
|
||||||
|
basic(t, "csi", term + " csi")
|
||||||
|
modifiers(t)
|
||||||
|
multiline(t)
|
||||||
|
cjk(t)
|
||||||
|
finally:
|
||||||
|
t.close()
|
||||||
|
print("FAILED:", len(FAILS))
|
||||||
|
for f in FAILS:
|
||||||
|
print(" -", f)
|
||||||
|
sys.exit(min(len(FAILS), 100))
|
||||||
+8
-1
@@ -162,7 +162,14 @@ static void parts_add_text(cJSON *parts, cJSON *txt) {
|
|||||||
cJSON_Delete(txt);
|
cJSON_Delete(txt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cJSON_AddItemToArray(parts, txt);
|
if (!txt->valuestring || !txt->valuestring[0]) {
|
||||||
|
cJSON_Delete(txt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cJSON *t = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(t, "type", "text");
|
||||||
|
cJSON_AddItemToObject(t, "text", txt);
|
||||||
|
cJSON_AddItemToArray(parts, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* One stored image as an image_url part, or NULL with why filled in. */
|
/* One stored image as an image_url part, or NULL with why filled in. */
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
#ifndef _DEFAULT_SOURCE
|
||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "cst_agent.h"
|
#include "cst_agent.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
/* Provider and model data live in model_registry.c (cst_models.h). */
|
/* Provider and model data live in model_registry.c (cst_models.h). */
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
"models": [
|
"models": [
|
||||||
{"match": "deepseek", "context": 1000000, "vision": false,
|
{"match": "deepseek", "context": 1000000, "vision": false,
|
||||||
"price": {"currency": "¥", "miss": 2, "hit": 0.04, "out": 8, "offpeak_half": true}},
|
"price": {"currency": "¥", "miss": 2, "hit": 0.04, "out": 8, "offpeak_half": true}},
|
||||||
|
{"match": "deepseek-flash", "vision": true},
|
||||||
|
{"match": "deepseek-chat", "vision": true},
|
||||||
|
{"match": "deepseek-reasoner", "vision": true},
|
||||||
{"match": "deepseek-v4-pro",
|
{"match": "deepseek-v4-pro",
|
||||||
"price": {"currency": "¥", "miss": 9, "hit": 0.3, "out": 27, "offpeak_half": true}},
|
"price": {"currency": "¥", "miss": 9, "hit": 0.3, "out": 27, "offpeak_half": true}},
|
||||||
{"match": "deepseek-vl", "vision": true},
|
{"match": "deepseek-vl", "vision": true},
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ typedef struct {
|
|||||||
char *prompt_hist[PROMPT_HIST_CAP];
|
char *prompt_hist[PROMPT_HIST_CAP];
|
||||||
int prompt_hist_n;
|
int prompt_hist_n;
|
||||||
int hist_browse;
|
int hist_browse;
|
||||||
|
char *hist_draft; /* the live input while browsing */
|
||||||
|
|
||||||
/* Busy-turn FIFO */
|
/* Busy-turn FIFO */
|
||||||
char *queue[QUEUE_CAP];
|
char *queue[QUEUE_CAP];
|
||||||
@@ -3163,28 +3164,16 @@ static size_t word_right(const tui_t *t, size_t at) {
|
|||||||
return at;
|
return at;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Move to the previous/next logical line keeping the column (in chars).
|
static size_t input_hit(const tui_t *t, int want_row, int want_col);
|
||||||
* Returns 0 when already on the first/last line. */
|
|
||||||
|
/* Move to the previous/next visual row (soft-wrapped or not) keeping the
|
||||||
|
* screen column. Returns 0 when already on the first/last row. */
|
||||||
static int input_line_move(tui_t *t, int dir) {
|
static int input_line_move(tui_t *t, int dir) {
|
||||||
size_t ls = line_start(t, t->cur);
|
int cr, cc;
|
||||||
int col = 0;
|
int rows = input_layout(t, NULL, NULL, &cr, &cc);
|
||||||
for (size_t i = ls; i < t->cur; i += next_len(t, i))
|
if (cr + dir < 0 || cr + dir >= rows)
|
||||||
col++;
|
return 0;
|
||||||
size_t target;
|
t->cur = input_hit(t, cr + dir, cc);
|
||||||
if (dir < 0) {
|
|
||||||
if (ls == 0)
|
|
||||||
return 0;
|
|
||||||
target = line_start(t, ls - 1);
|
|
||||||
} else {
|
|
||||||
size_t le = line_end(t, t->cur);
|
|
||||||
if (le >= t->in_len)
|
|
||||||
return 0;
|
|
||||||
target = le + 1;
|
|
||||||
}
|
|
||||||
size_t te = line_end(t, target);
|
|
||||||
while (col-- > 0 && target < te)
|
|
||||||
target += next_len(t, target);
|
|
||||||
t->cur = target;
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5854,7 +5843,7 @@ static int submit_input(tui_t *t, int busy) {
|
|||||||
static size_t input_hit(const tui_t *t, int want_row, int want_col) {
|
static size_t input_hit(const tui_t *t, int want_row, int want_col) {
|
||||||
int row = 0, col = IN_COL;
|
int row = 0, col = IN_COL;
|
||||||
int right = (t->in_w > 0 ? t->in_w : t->cols) - 2;
|
int right = (t->in_w > 0 ? t->in_w : t->cols) - 2;
|
||||||
size_t i = 0, best = t->in_len;
|
size_t i = 0, best = t->in_len, last = 0;
|
||||||
while (i < t->in_len) {
|
while (i < t->in_len) {
|
||||||
if (row == want_row && col >= want_col)
|
if (row == want_row && col >= want_col)
|
||||||
return i;
|
return i;
|
||||||
@@ -5873,12 +5862,14 @@ static size_t input_hit(const tui_t *t, int want_row, int want_col) {
|
|||||||
utf8_decode(t->inbuf + i, t->in_len - i, &wc, &eaten);
|
utf8_decode(t->inbuf + i, t->in_len - i, &wc, &eaten);
|
||||||
int cw = utf8_width(wc == L'\t' ? L' ' : wc);
|
int cw = utf8_width(wc == L'\t' ? L' ' : wc);
|
||||||
if (col + cw > right && col > IN_COL) {
|
if (col + cw > right && col > IN_COL) {
|
||||||
|
/* offset i would be drawn at the start of the next row */
|
||||||
if (row == want_row)
|
if (row == want_row)
|
||||||
return i;
|
return last;
|
||||||
row++;
|
row++;
|
||||||
col = IN_COL;
|
col = IN_COL;
|
||||||
}
|
}
|
||||||
best = i + eaten;
|
best = i + eaten;
|
||||||
|
last = i;
|
||||||
col += cw;
|
col += cw;
|
||||||
i += eaten;
|
i += eaten;
|
||||||
}
|
}
|
||||||
@@ -6395,29 +6386,65 @@ static int g_k_alt_up, g_k_alt_down, g_k_ctrl_home, g_k_ctrl_end,
|
|||||||
g_k_ctrl_up, g_k_ctrl_down, g_k_ctrl_left, g_k_ctrl_right, g_k_alt_left,
|
g_k_ctrl_up, g_k_ctrl_down, g_k_ctrl_left, g_k_ctrl_right, g_k_alt_left,
|
||||||
g_k_alt_right, g_k_ctrl_dc, g_k_alt_dc;
|
g_k_alt_right, g_k_ctrl_dc, g_k_alt_dc;
|
||||||
|
|
||||||
static int ext_key(const char *cap) {
|
/* The terminfo key, else the xterm sequence bound to our own code `own`. */
|
||||||
|
static int ext_key(const char *cap, const char *xterm, int own) {
|
||||||
char *s = tigetstr(cap);
|
char *s = tigetstr(cap);
|
||||||
if (!s || s == (char *)-1)
|
int k = s && s != (char *)-1 ? key_defined(s) : 0;
|
||||||
return 0;
|
if (k > 0)
|
||||||
int k = key_defined(s);
|
return k;
|
||||||
return k > 0 ? k : 0;
|
#ifndef _WIN32
|
||||||
|
k = key_defined(xterm);
|
||||||
|
if (k > 0)
|
||||||
|
return k;
|
||||||
|
if (k == 0 && define_key(xterm, own) == OK)
|
||||||
|
return own;
|
||||||
|
#else
|
||||||
|
(void)xterm, (void)own;
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(NCURSES_VERSION) && !defined(_WIN32)
|
||||||
|
/* Terminfo lists one form per key, but terminals disagree: cursor keys come
|
||||||
|
* as CSI or SS3 whatever DECCKM says (tmux, mosh, a wrong TERM), Home/End as
|
||||||
|
* CSI H, SS3 H or CSI 1~/7~. Unknown forms would be read as Esc + text. */
|
||||||
|
static void vt_keys_init(void) {
|
||||||
|
static const struct {
|
||||||
|
const char *seq;
|
||||||
|
int key;
|
||||||
|
} K[] = {
|
||||||
|
{"\033[A", KEY_UP}, {"\033OA", KEY_UP}, {"\033[B", KEY_DOWN},
|
||||||
|
{"\033OB", KEY_DOWN}, {"\033[C", KEY_RIGHT}, {"\033OC", KEY_RIGHT},
|
||||||
|
{"\033[D", KEY_LEFT}, {"\033OD", KEY_LEFT}, {"\033[H", KEY_HOME},
|
||||||
|
{"\033OH", KEY_HOME}, {"\033[1~", KEY_HOME}, {"\033[7~", KEY_HOME},
|
||||||
|
{"\033[F", KEY_END}, {"\033OF", KEY_END}, {"\033[4~", KEY_END},
|
||||||
|
{"\033[8~", KEY_END}, {"\033[2~", KEY_IC}, {"\033[3~", KEY_DC},
|
||||||
|
{"\033[5~", KEY_PPAGE}, {"\033[6~", KEY_NPAGE}, {"\033[Z", KEY_BTAB},
|
||||||
|
{"\033[1;2A", KEY_SR}, {"\033[1;2B", KEY_SF}, {"\033[1;2C", KEY_SRIGHT},
|
||||||
|
{"\033[1;2D", KEY_SLEFT},
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(K) / sizeof(K[0]); i++)
|
||||||
|
if (key_defined(K[i].seq) == 0)
|
||||||
|
define_key(K[i].seq, K[i].key);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void keys_init(void) {
|
static void keys_init(void) {
|
||||||
#ifdef NCURSES_VERSION
|
#ifdef NCURSES_VERSION
|
||||||
g_k_alt_up = ext_key("kUP3");
|
int own = KEY_OWN_BASE + 4; /* up to KEY_KITTY_BASE - 1 */
|
||||||
g_k_alt_down = ext_key("kDN3");
|
g_k_alt_up = ext_key("kUP3", "\033[1;3A", own++);
|
||||||
g_k_ctrl_home = ext_key("kHOM5");
|
g_k_alt_down = ext_key("kDN3", "\033[1;3B", own++);
|
||||||
g_k_ctrl_end = ext_key("kEND5");
|
g_k_ctrl_home = ext_key("kHOM5", "\033[1;5H", own++);
|
||||||
g_k_ctrl_up = ext_key("kUP5");
|
g_k_ctrl_end = ext_key("kEND5", "\033[1;5F", own++);
|
||||||
g_k_ctrl_down = ext_key("kDN5");
|
g_k_ctrl_up = ext_key("kUP5", "\033[1;5A", own++);
|
||||||
g_k_ctrl_left = ext_key("kLFT5");
|
g_k_ctrl_down = ext_key("kDN5", "\033[1;5B", own++);
|
||||||
g_k_ctrl_right = ext_key("kRIT5");
|
g_k_ctrl_left = ext_key("kLFT5", "\033[1;5D", own++);
|
||||||
g_k_alt_left = ext_key("kLFT3");
|
g_k_ctrl_right = ext_key("kRIT5", "\033[1;5C", own++);
|
||||||
g_k_alt_right = ext_key("kRIT3");
|
g_k_alt_left = ext_key("kLFT3", "\033[1;3D", own++);
|
||||||
g_k_ctrl_dc = ext_key("kDC5");
|
g_k_alt_right = ext_key("kRIT3", "\033[1;3C", own++);
|
||||||
g_k_alt_dc = ext_key("kDC3");
|
g_k_ctrl_dc = ext_key("kDC5", "\033[3;5~", own++);
|
||||||
|
g_k_alt_dc = ext_key("kDC3", "\033[3;3~", own++);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6630,6 +6657,7 @@ static void tui_free(tui_t *t) {
|
|||||||
queue_free_all(t);
|
queue_free_all(t);
|
||||||
for (int i = 0; i < t->prompt_hist_n; i++)
|
for (int i = 0; i < t->prompt_hist_n; i++)
|
||||||
free(t->prompt_hist[i]);
|
free(t->prompt_hist[i]);
|
||||||
|
free(t->hist_draft);
|
||||||
for (int i = 0; i < t->nfiles; i++)
|
for (int i = 0; i < t->nfiles; i++)
|
||||||
free(t->files[i]);
|
free(t->files[i]);
|
||||||
free(t->files);
|
free(t->files);
|
||||||
@@ -6720,6 +6748,7 @@ int cst_tui_run(cst_agent_t *agent) {
|
|||||||
/* the Windows console driver reads key events, not VT sequences */
|
/* the Windows console driver reads key events, not VT sequences */
|
||||||
define_key("\033[200~", KEY_PASTE_BEGIN);
|
define_key("\033[200~", KEY_PASTE_BEGIN);
|
||||||
define_key("\033[201~", KEY_PASTE_END);
|
define_key("\033[201~", KEY_PASTE_END);
|
||||||
|
vt_keys_init();
|
||||||
printf("\033[?2004h");
|
printf("\033[?2004h");
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
kitty_init();
|
kitty_init();
|
||||||
@@ -7032,15 +7061,22 @@ int cst_tui_run(cst_agent_t *agent) {
|
|||||||
if (t.prompt_hist_n <= 0)
|
if (t.prompt_hist_n <= 0)
|
||||||
continue;
|
continue;
|
||||||
int idx = t.hist_browse;
|
int idx = t.hist_browse;
|
||||||
|
if (idx < 0) {
|
||||||
|
free(t.hist_draft);
|
||||||
|
t.hist_draft = strdup(t.inbuf);
|
||||||
|
}
|
||||||
idx = idx < 0 ? t.prompt_hist_n - 1 : (idx > 0 ? idx - 1 : 0);
|
idx = idx < 0 ? t.prompt_hist_n - 1 : (idx > 0 ? idx - 1 : 0);
|
||||||
hist_load_into_input(&t, idx);
|
hist_load_into_input(&t, idx);
|
||||||
} else {
|
} else {
|
||||||
if (t.hist_browse < 0)
|
if (t.hist_browse < 0)
|
||||||
continue;
|
continue;
|
||||||
if (t.hist_browse + 1 >= t.prompt_hist_n)
|
if (t.hist_browse + 1 >= t.prompt_hist_n) {
|
||||||
input_clear(&t);
|
input_set(&t, t.hist_draft);
|
||||||
else
|
free(t.hist_draft);
|
||||||
|
t.hist_draft = NULL;
|
||||||
|
} else {
|
||||||
hist_load_into_input(&t, t.hist_browse + 1);
|
hist_load_into_input(&t, t.hist_browse + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
after_edit(&t);
|
after_edit(&t);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <io.h>
|
#include <io.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#else
|
#else
|
||||||
|
#include <poll.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -177,7 +178,7 @@ static CST_TEST_UNUSED void test_sleep_ms(int ms) {
|
|||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
Sleep((DWORD)ms);
|
Sleep((DWORD)ms);
|
||||||
#else
|
#else
|
||||||
usleep((useconds_t)ms * 1000);
|
poll(NULL, 0, ms); /* usleep is hidden by -std=c11 on musl */
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "cst_agent.h"
|
#include "cst_agent.h"
|
||||||
|
#include "cst_attach.h"
|
||||||
|
#include "cst_test_util.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -325,8 +327,88 @@ static void test_images_off(void) {
|
|||||||
cJSON_Delete(j);
|
cJSON_Delete(j);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Every element of every content array must be a typed part object. */
|
||||||
|
static int parts_typed(const cJSON *root) {
|
||||||
|
const cJSON *m;
|
||||||
|
cJSON_ArrayForEach(m, cJSON_GetObjectItemCaseSensitive(root, "messages")) {
|
||||||
|
const cJSON *c = cJSON_GetObjectItemCaseSensitive(m, "content");
|
||||||
|
if (!cJSON_IsArray(c))
|
||||||
|
continue;
|
||||||
|
const cJSON *p;
|
||||||
|
cJSON_ArrayForEach(p, c) {
|
||||||
|
const cJSON *t = cJSON_GetObjectItemCaseSensitive(p, "type");
|
||||||
|
if (!cJSON_IsObject(p) || !cJSON_IsString(t))
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_images_on(void) {
|
||||||
|
char dir[1024], sess[1100], err[256];
|
||||||
|
if (test_tmpdir(dir, sizeof(dir), "cst-body") != 0) {
|
||||||
|
EXPECT(0, "scratch dir");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snprintf(sess, sizeof(sess), "%s/s.jsonl", dir);
|
||||||
|
static const unsigned char png[32] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A,
|
||||||
|
'\n', 0, 0, 0, 13, 'I', 'H', 'D', 'R',
|
||||||
|
0, 0, 0, 2, 0, 0, 0, 2};
|
||||||
|
cst_attachment_t *img = cst_attach_import_bytes(sess, "shot.png",
|
||||||
|
(const char *)png,
|
||||||
|
sizeof(png), err, sizeof(err));
|
||||||
|
EXPECT(img != NULL, "image stored");
|
||||||
|
if (!img)
|
||||||
|
return;
|
||||||
|
cst_attachment_t gone = *img;
|
||||||
|
snprintf(gone.rel, sizeof(gone.rel), "missing.png");
|
||||||
|
cst_message_t m[5];
|
||||||
|
m[0] = msg(CST_ROLE_USER, "what is this");
|
||||||
|
m[0].attach = img;
|
||||||
|
m[0].nattach = 1;
|
||||||
|
m[1] = msg(CST_ROLE_ASSISTANT, "");
|
||||||
|
m[1].tool_calls_json = "[{\"id\":\"c1\",\"type\":\"function\",\"function\":"
|
||||||
|
"{\"name\":\"view_image\",\"arguments\":\"{}\"}}]";
|
||||||
|
m[2] = msg(CST_ROLE_TOOL, "view_image shot.png");
|
||||||
|
snprintf(m[2].tool_call_id, sizeof(m[2].tool_call_id), "c1");
|
||||||
|
m[2].attach = img;
|
||||||
|
m[2].nattach = 1;
|
||||||
|
m[3] = msg(CST_ROLE_USER, "and now?");
|
||||||
|
m[4] = msg(CST_ROLE_USER, "");
|
||||||
|
m[4].attach = &gone;
|
||||||
|
m[4].nattach = 1;
|
||||||
|
cst_llm_request_t r;
|
||||||
|
memset(&r, 0, sizeof(r));
|
||||||
|
r.model = "deepseek-flash";
|
||||||
|
r.msgs = m;
|
||||||
|
r.nmsgs = 5;
|
||||||
|
r.session_path = sess;
|
||||||
|
cJSON *j = build_req(&r);
|
||||||
|
EXPECT(parts_typed(j), "content arrays hold only typed parts");
|
||||||
|
const cJSON *p0 = cJSON_GetObjectItemCaseSensitive(
|
||||||
|
cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(j, "messages"), 0),
|
||||||
|
"content");
|
||||||
|
const cJSON *t0 = cJSON_GetObjectItemCaseSensitive(
|
||||||
|
cJSON_GetArrayItem(p0, 0), "text");
|
||||||
|
const cJSON *u1 = cJSON_GetObjectItemCaseSensitive(
|
||||||
|
cJSON_GetObjectItemCaseSensitive(cJSON_GetArrayItem(p0, 1), "image_url"),
|
||||||
|
"url");
|
||||||
|
EXPECT(cJSON_IsString(t0) && strcmp(t0->valuestring, "what is this") == 0,
|
||||||
|
"user text is a text part");
|
||||||
|
EXPECT(cJSON_IsString(u1) &&
|
||||||
|
strncmp(u1->valuestring, "data:image/png;base64,", 22) == 0,
|
||||||
|
"image is a data URL part");
|
||||||
|
char *s = cJSON_PrintUnformatted(j);
|
||||||
|
EXPECT(s && strstr(s, "and now?") && strstr(s, "not sent"),
|
||||||
|
"follow-up text and the missing-image note are parts too");
|
||||||
|
free(s);
|
||||||
|
cJSON_Delete(j);
|
||||||
|
cst_attach_free(img);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
test_images_off();
|
test_images_off();
|
||||||
|
test_images_on();
|
||||||
test_reasoning_and_prefix();
|
test_reasoning_and_prefix();
|
||||||
test_cache_hints();
|
test_cache_hints();
|
||||||
test_tools_snapshot();
|
test_tools_snapshot();
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ static void test_builtin(void) {
|
|||||||
EXPECT(mi.has_price && mi.price.miss == 9.0 && mi.price.offpeak_half,
|
EXPECT(mi.has_price && mi.price.miss == 9.0 && mi.price.offpeak_half,
|
||||||
"longer match wins the price");
|
"longer match wins the price");
|
||||||
EXPECT(strcmp(mi.source, "built-in") == 0, "source built-in");
|
EXPECT(strcmp(mi.source, "built-in") == 0, "source built-in");
|
||||||
|
cst_model_info("deepseek", "deepseek-flash", &mi);
|
||||||
|
EXPECT(mi.context == 1000000 && mi.vision == 1, "deepseek-flash takes images");
|
||||||
|
cst_model_info("deepseek", "deepseek-chat", &mi);
|
||||||
|
EXPECT(mi.vision == 1, "deepseek-chat is deepseek-flash");
|
||||||
cst_model_info(NULL, "gpt-4o-mini-2024", &mi);
|
cst_model_info(NULL, "gpt-4o-mini-2024", &mi);
|
||||||
EXPECT(mi.price.miss == 0.15 && mi.context == 128000 && mi.vision == 1,
|
EXPECT(mi.price.miss == 0.15 && mi.context == 128000 && mi.vision == 1,
|
||||||
"gpt-4o-mini over gpt-4o");
|
"gpt-4o-mini over gpt-4o");
|
||||||
@@ -152,7 +156,7 @@ static void test_cache(void) {
|
|||||||
strcmp(mi.price.currency, "$") == 0,
|
strcmp(mi.price.currency, "$") == 0,
|
||||||
"API vision and USD price");
|
"API vision and USD price");
|
||||||
cst_model_info("deepseek", "deepseek-flash", &mi);
|
cst_model_info("deepseek", "deepseek-flash", &mi);
|
||||||
EXPECT(mi.vision == 0 && mi.price.miss == 2.0,
|
EXPECT(mi.price.miss == 2.0 && strcmp(mi.price.currency, "¥") == 0,
|
||||||
"another provider's cache is not used");
|
"another provider's cache is not used");
|
||||||
EXPECT(cst_model_context_tokens("anthropic/claude-sonnet-4") == 1000000,
|
EXPECT(cst_model_context_tokens("anthropic/claude-sonnet-4") == 1000000,
|
||||||
"no provider: any cache, exact id");
|
"no provider: any cache, exact id");
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ static void test_vision(const char *root) {
|
|||||||
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", root);
|
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", root);
|
||||||
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/blind.jsonl", dir);
|
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/blind.jsonl", dir);
|
||||||
snprintf(cfg.api_base, sizeof(cfg.api_base), "http://replay/v1");
|
snprintf(cfg.api_base, sizeof(cfg.api_base), "http://replay/v1");
|
||||||
snprintf(cfg.model, sizeof(cfg.model), "deepseek-flash");
|
snprintf(cfg.model, sizeof(cfg.model), "deepseek-v4-pro");
|
||||||
snprintf(cfg.provider, sizeof(cfg.provider), "deepseek");
|
snprintf(cfg.provider, sizeof(cfg.provider), "deepseek");
|
||||||
cfg.protocol = CST_PROTO_NATIVE_EOS;
|
cfg.protocol = CST_PROTO_NATIVE_EOS;
|
||||||
cfg.max_steps = 4;
|
cfg.max_steps = 4;
|
||||||
@@ -220,7 +220,7 @@ static void test_vision(const char *root) {
|
|||||||
EXPECT(a != NULL, "blind agent");
|
EXPECT(a != NULL, "blind agent");
|
||||||
if (!a)
|
if (!a)
|
||||||
return;
|
return;
|
||||||
EXPECT(cst_agent_sees_images(a) == 0, "deepseek-flash is text only");
|
EXPECT(cst_agent_sees_images(a) == 0, "deepseek-v4-pro is text only");
|
||||||
EXPECT(cst_agent_attach_bytes(a, "shot.png", (const char *)png, pn, err,
|
EXPECT(cst_agent_attach_bytes(a, "shot.png", (const char *)png, pn, err,
|
||||||
sizeof(err)) == 0,
|
sizeof(err)) == 0,
|
||||||
"attach to a blind model");
|
"attach to a blind model");
|
||||||
|
|||||||
Reference in New Issue
Block a user