From de2e08257d704a3b7aa6aa82df1e4f044212013d Mon Sep 17 00:00:00 2001 From: shanjunjie Date: Mon, 6 Jul 2026 01:54:53 +0800 Subject: [PATCH] fix(dsml): preserve schema strings and robust e2e reads --- proxy/dsml_shim.py | 2 ++ test/test_dsml_shim.py | 8 ++++++ test/test_proxy_dsml_e2e.py | 50 ++++++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/proxy/dsml_shim.py b/proxy/dsml_shim.py index 802ee07..23099cb 100644 --- a/proxy/dsml_shim.py +++ b/proxy/dsml_shim.py @@ -61,6 +61,8 @@ def _coerce_param(pname, string_attr, raw, prop_schema): if string_attr == "true": return raw typ = (prop_schema or {}).get("type") + if typ == "string": + return raw try: if typ == "integer": return int(raw) diff --git a/test/test_dsml_shim.py b/test/test_dsml_shim.py index 9ec998e..164453c 100644 --- a/test/test_dsml_shim.py +++ b/test/test_dsml_shim.py @@ -110,6 +110,14 @@ def test_string_true_stays_string_even_if_numeric(self): blk = wrap_typed(P2, "calc", [("n", ' string="true"', "42")]) self.assertEqual(ds.parse_dsml_tool_calls(blk, NUM)[0]["input"]["n"], "42") + def test_schema_string_stays_string_without_string_attr(self): + # DeepSeek can omit string="true" on string parameters. Schema type string + # must win over the JSON fallback, or numeric-looking text is coerced and + # the whole tool call gets discarded by validation. + blk = wrap_typed(P2, "web_search", [("query", "", "42")]) + self.assertEqual(ds.parse_dsml_tool_calls(blk, WS), + [{"name": "web_search", "input": {"query": "42"}}]) + def test_missing_string_attr_falls_back_json_then_str(self): blk = wrap_typed(P2, "calc", [("f", "", "3.5")]) # f 是 number → 3.5;无 schema 属性的 json 兜底另测 diff --git a/test/test_proxy_dsml_e2e.py b/test/test_proxy_dsml_e2e.py index 837eef2..045e946 100644 --- a/test/test_proxy_dsml_e2e.py +++ b/test/test_proxy_dsml_e2e.py @@ -77,7 +77,7 @@ def serve(): c, _ = srv.accept() except OSError: return - req = c.recv(65536) + req = _read_http_request(c) is_stream = b'"stream": true' in req or b'"stream":true' in req if is_stream: sse = _build_sse() @@ -95,6 +95,54 @@ def serve(): return f"http://127.0.0.1:{port}/up", srv +def _read_http_request(sock): + data = b"" + while b"\r\n\r\n" not in data: + chunk = sock.recv(65536) + if not chunk: + return data + data += chunk + + head, sep, body = data.partition(b"\r\n\r\n") + content_length = 0 + for line in head.split(b"\r\n"): + name, colon, value = line.partition(b":") + if colon and name.strip().lower() == b"content-length": + try: + content_length = int(value.strip()) + except ValueError: + content_length = 0 + break + + while len(body) < content_length: + chunk = sock.recv(65536) + if not chunk: + break + body += chunk + return head + sep + body + + +class _ChunkedSocket: + def __init__(self, chunks): + self._chunks = list(chunks) + + def recv(self, _size): + if not self._chunks: + return b"" + return self._chunks.pop(0) + + +class HttpRequestReader(unittest.TestCase): + def test_reads_body_split_after_headers(self): + body = b'{"stream": true, "messages":["large enough to split"]}' + head = (b"POST /up HTTP/1.1\r\nHost: 127.0.0.1\r\n" + b"Content-Type: application/json\r\n" + + f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")) + sock = _ChunkedSocket([head[:17], head[17:], body[:9], body[9:]]) + req = _read_http_request(sock) + self.assertEqual(req, head + body) + + def raw_post(host, port, path, body): s = socket.create_connection((host, port), timeout=5) req = (f"POST {path} HTTP/1.1\r\nHost: {host}\r\n"