mnemonic-proxy 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,304 @@
1
+ # Author: Maurizio Verde — LastReload
2
+ """Motore finto HTTP (solo stdlib) per i test, la demo e `check` in CI: OpenAI /v1/chat/completions + /slots/0.
3
+
4
+ - Registra ogni richiesta ricevuta (self.requests) e il prompt "fisico" renderizzato in modo deterministico.
5
+ - Simula la cache a prefisso: cached_tokens = prefisso comune (in caratteri / 3.5) col prompt precedente;
6
+ timings.prompt_n = token letti davvero. Così il giornale del proxy riceve reused/prompt_read come da Strata.
7
+ - `policy(body) -> message` decide la risposta (default: testo fisso). Le richieste di note e di sigillo sono
8
+ riconosciute dal loro marcatore.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import threading
14
+ import time
15
+ import uuid
16
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
17
+
18
+
19
+ def render(body: dict) -> str:
20
+ """Rendering deterministico del prompt (stand-in del template chat): ciò che conta è che due liste di messaggi
21
+ uguali diano la stessa stringa e che un messaggio cambiato cambi la stringa da quel punto in poi."""
22
+ out = ["<tools>" + json.dumps(body.get("tools") or [], sort_keys=True, ensure_ascii=False) + "</tools>"]
23
+ for m in body.get("messages") or []:
24
+ out.append("<|im_start|>%s\n%s%s%s<|im_end|>\n" % (
25
+ m.get("role"), json.dumps(m.get("content"), ensure_ascii=False),
26
+ json.dumps(m.get("tool_calls"), sort_keys=True, ensure_ascii=False) if m.get("tool_calls") else "",
27
+ m.get("tool_call_id") or ""))
28
+ return "".join(out)
29
+
30
+
31
+ class FakeEngine:
32
+ def __init__(self, policy=None, cpt: float = 3.5, flavor: str = "strata", slot_save: bool = True,
33
+ n_slots: int = 1, n_ctx: int = 131072):
34
+ """flavor: strata (/v1/status) | llama (llama-server: /props, /slots, /tokenize, 400 per file mancante,
35
+ id_task che cresce di più di 1 per richiesta, 501 sulle azioni slot senza --slot-save-path) | ds4
36
+ (ds4-server: /v1/models con owned_by "ds4.c", niente slot, id `call_<hex>` generati dal motore, 400 su un
37
+ risultato di strumento con id sconosciuto, conteggio delle chiamate rimandate con id noto/sconosciuto) | openai
38
+ (solo /v1/chat/completions)."""
39
+ self.flavor, self.slot_save, self.n_slots, self.n_ctx = flavor, slot_save, n_slots, n_ctx
40
+ self.id_task = 0
41
+ self.policy = policy or (lambda body: {"role": "assistant", "content": "ok"})
42
+ self.requests: list[dict] = []
43
+ self.prompts: list[str] = []
44
+ self.slots: list[tuple] = []
45
+ self.files: dict[str, str] = {}
46
+ self.cancelled = 0
47
+ self.cpt = cpt
48
+ self.live = ""
49
+ self.srv = None
50
+ self.started = 1000
51
+ self.n_requests = 0
52
+ self.busy = False
53
+ self.ds4_memory: dict = {} # flavor ds4: id -> argomenti campionati (exact DSML tool replay)
54
+ self.ds4_live_ids: set = set()
55
+ self.ds4_replay = {"mem": 0, "canonical": 0, "missing_ids": []}
56
+ self.rejected: list = []
57
+
58
+ def restart(self):
59
+ """Come un riavvio di Strata: la conversazione in memoria si perde, i file di sessione restano."""
60
+ self.live = ""
61
+ self.started += 1
62
+ self.n_requests = 0
63
+ self.id_task = 0
64
+
65
+ def slots_list(self) -> list:
66
+ return [{"id": i, "n_ctx": self.n_ctx // self.n_slots, "is_processing": bool(self.busy) and i == 0,
67
+ "id_task": self.id_task if i == 0 else -1, "n_prompt_tokens": int(len(self.live) / self.cpt)}
68
+ for i in range(self.n_slots)]
69
+
70
+ def status(self) -> dict:
71
+ return {"service": "strata", "loaded": True, "started": self.started,
72
+ "context": {"native": self.n_ctx, "max_positions": self.n_ctx},
73
+ "activity": {"requests": self.n_requests, "in_flight": int(self.busy)}}
74
+
75
+ # ---- ds4-server (flavor="ds4"): comportamento verificato su ds4_server.c ----
76
+ def ds4_check(self, body: dict) -> str | None:
77
+ """Come anthropic_validate_tool_results / responses_validate_tool_outputs: un risultato di strumento con un
78
+ id che non è né nella storia rimandata (chiamata assistant precedente) né nello stato vivo -> errore
79
+ «replay full history». (Il vero ds4 lo fa su /v1/messages e /v1/responses; qui anche su chat, più severo.)"""
80
+ seen = set()
81
+ for m in body.get("messages") or []:
82
+ if m.get("role") == "assistant":
83
+ seen.update(c.get("id") for c in m.get("tool_calls") or [])
84
+ elif m.get("role") == "tool":
85
+ cid = m.get("tool_call_id")
86
+ if cid not in seen and cid not in self.ds4_live_ids:
87
+ return ("continuation state is not available for tool_call_id %s; retry by replaying the full "
88
+ "messages history" % cid)
89
+ return None
90
+
91
+ def ds4_after(self, body: dict, msg: dict) -> dict:
92
+ """Ripresa per id: per ogni tool call rimandata, id noto = testo campionato esatto (mem), id sconosciuto =
93
+ rendering canonico (il prefisso può cambiare). Poi id nuovi `call_<hex>` alle chiamate del modello."""
94
+ for m in body.get("messages") or []:
95
+ for c in (m.get("tool_calls") or []) if m.get("role") == "assistant" else []:
96
+ if c.get("id") in self.ds4_memory:
97
+ self.ds4_replay["mem"] += 1
98
+ else:
99
+ self.ds4_replay["canonical"] += 1
100
+ self.ds4_replay["missing_ids"].append(c.get("id"))
101
+ calls = []
102
+ for c in msg.get("tool_calls") or []:
103
+ c = dict(c)
104
+ if not c.get("id"):
105
+ c["id"] = "call_" + uuid.uuid4().hex
106
+ self.ds4_memory[c["id"]] = (c.get("function") or {}).get("arguments")
107
+ calls.append(c)
108
+ self.ds4_live_ids = {c["id"] for c in calls}
109
+ return {**msg, "tool_calls": calls} if calls else msg
110
+
111
+ def handle_chat(self, body: dict) -> dict:
112
+ p = render(body)
113
+ common = 0
114
+ for a, b in zip(self.live, p):
115
+ if a != b:
116
+ break
117
+ common += 1
118
+ self.requests.append(body)
119
+ self.prompts.append(p)
120
+ self.n_requests += 1
121
+ self.id_task += 3
122
+ msg = self.policy(body)
123
+ if self.flavor == "ds4":
124
+ msg = self.ds4_after(body, msg)
125
+ self.live = p
126
+ pt = int(len(p) / self.cpt)
127
+ cached = int(common / self.cpt)
128
+ finish = "tool_calls" if msg.get("tool_calls") else "stop"
129
+ ct = int(len(json.dumps(msg)) / self.cpt)
130
+ return {"id": "chatcmpl-" + uuid.uuid4().hex[:8], "object": "chat.completion", "created": int(time.time()),
131
+ "model": "fake", "choices": [{"index": 0, "message": msg, "finish_reason": finish}],
132
+ "usage": {"prompt_tokens": pt, "completion_tokens": ct, "total_tokens": pt + ct,
133
+ "prompt_tokens_details": {"cached_tokens": cached}},
134
+ "timings": {"cache_n": cached, "prompt_n": pt - cached, "prompt_ms": (pt - cached) * 0.4}}
135
+
136
+ def start(self, port: int = 0):
137
+ eng = self
138
+
139
+ class H(BaseHTTPRequestHandler):
140
+ protocol_version = "HTTP/1.1"
141
+
142
+ def log_message(self, *a):
143
+ pass
144
+
145
+ def _send(self, st, obj):
146
+ d = json.dumps(obj).encode()
147
+ self.send_response(st)
148
+ self.send_header("Content-Type", "application/json")
149
+ self.send_header("Content-Length", str(len(d)))
150
+ self.end_headers()
151
+ self.wfile.write(d)
152
+
153
+ def do_GET(self):
154
+ if eng.flavor == "llama":
155
+ if self.path.startswith("/props"):
156
+ return self._send(200, {"default_generation_settings": {"n_ctx": eng.n_ctx // eng.n_slots},
157
+ "total_slots": eng.n_slots, "model_path": "/m/fake-0.6B.gguf"})
158
+ if self.path.startswith("/slots"):
159
+ return self._send(200, eng.slots_list())
160
+ if self.path.startswith("/health"):
161
+ return self._send(200, {"status": "ok"})
162
+ if self.path.startswith("/v1/models"):
163
+ return self._send(200, {"object": "list", "data": [{"id": "fake-0.6B.gguf"}]})
164
+ return self._send(404, {"error": {"code": 404, "message": "File Not Found"}})
165
+ if eng.flavor == "openai":
166
+ if self.path.startswith("/v1/models"):
167
+ return self._send(200, {"data": [{"id": "fake"}]})
168
+ return self._send(404, {"error": {"message": "not found"}})
169
+ if eng.flavor == "ds4":
170
+ if self.path == "/v1/models":
171
+ mk = lambda i: {"id": i, "object": "model", "created": 1767225600, "owned_by": "ds4.c", # noqa
172
+ "name": "Qwen3.8-Flash-Next", "context_length": eng.n_ctx,
173
+ "top_provider": {"context_length": eng.n_ctx}}
174
+ return self._send(200, {"object": "list", "data": [
175
+ mk("qwen3.8-flash-next"), mk("qwen3.8-flash-next-chat"),
176
+ mk("qwen3.8-flash-next-reasoner")]})
177
+ return self._send(404, {"error": {"message": "unknown endpoint"}})
178
+ if self.path.startswith("/v1/status"):
179
+ return self._send(200, eng.status())
180
+ self._send(200, {"status": "ok", "fake": True})
181
+
182
+ def do_POST(self):
183
+ n = int(self.headers.get("Content-Length") or 0)
184
+ body = json.loads(self.rfile.read(n) or b"{}")
185
+ if eng.flavor == "openai" and not self.path.startswith("/v1/chat"):
186
+ return self._send(404, {"error": {"message": "not found"}})
187
+ if eng.flavor == "ds4":
188
+ if not self.path.startswith("/v1/chat/completions"):
189
+ eng.slots.append(("unexpected", self.path, len(eng.live)))
190
+ return self._send(404, {"error": {"message": "unknown endpoint"}})
191
+ err = eng.ds4_check(body)
192
+ if err:
193
+ eng.rejected.append(err)
194
+ return self._send(400, {"error": {"message": err, "type": "invalid_request_error"}})
195
+ if eng.flavor == "llama" and self.path.startswith("/tokenize"):
196
+ return self._send(200, {"tokens": list(range(int(len(body.get("content") or "") / eng.cpt)))})
197
+ if eng.flavor == "llama" and self.path.startswith("/slots/"):
198
+ sid = int(self.path.split("/")[2].split("?")[0])
199
+ action = self.path.split("action=")[-1]
200
+ if not eng.slot_save:
201
+ return self._send(501, {"error": {"code": 501, "message": "This server does not support "
202
+ "slots action. Start it with `--slot-save-path`"}})
203
+ if action not in ("save", "restore", "erase"):
204
+ return self._send(400, {"error": {"code": 400, "message": "Invalid action"}})
205
+ if sid != 0:
206
+ return self._send(400, {"error": {"code": 400, "message": "Invalid slot ID"}})
207
+ fn = body.get("filename")
208
+ eng.slots.append((action, fn, len(eng.live)))
209
+ if action == "save":
210
+ eng.files[fn] = eng.live
211
+ return self._send(200, {"id_slot": sid, "filename": fn, "n_saved": int(len(eng.live) / eng.cpt),
212
+ "n_written": len(eng.live) * 10, "timings": {"save_ms": 1.0}})
213
+ if action == "restore":
214
+ if fn not in eng.files:
215
+ return self._send(400, {"error": {"code": 400, "message": "Unable to restore slot: No "
216
+ "available space in KV cache or invalid slot save file"}})
217
+ eng.live = eng.files[fn]
218
+ return self._send(200, {"id_slot": sid, "filename": fn,
219
+ "n_restored": int(len(eng.live) / eng.cpt),
220
+ "n_read": len(eng.live) * 10, "timings": {"restore_ms": 1.0}})
221
+ eng.live = ""
222
+ return self._send(200, {"id_slot": sid, "n_erased": 1})
223
+ if self.path.startswith("/slots/0"):
224
+ action = self.path.split("action=")[-1]
225
+ eng.slots.append((action, body.get("filename"), len(eng.live)))
226
+ if action == "save":
227
+ eng.files[body.get("filename")] = eng.live
228
+ elif action == "restore":
229
+ if body.get("filename") not in eng.files:
230
+ return self._send(404, {"error": {"message": "file non trovato"}})
231
+ eng.live = eng.files[body.get("filename")]
232
+ return self._send(200, {"id_slot": 0, "filename": body.get("filename"),
233
+ "n_saved": int(len(eng.live) / eng.cpt),
234
+ "n_restored": int(len(eng.live) / eng.cpt),
235
+ "n_written": len(eng.live) * 10, "timings": {"save_ms": 1.0}})
236
+ if body.get("stream"):
237
+ return self.do_POST_stream(body)
238
+ self._send(200, eng.handle_chat(body))
239
+
240
+ def do_POST_stream(self, body):
241
+ r = eng.handle_chat(body)
242
+ msg = dict(r["choices"][0]["message"])
243
+ slow = msg.pop("_slow", 0)
244
+ base = {k: r[k] for k in ("id", "object", "created", "model")}
245
+ base["object"] = "chat.completion.chunk"
246
+
247
+ def ch(delta, fin=None, **x):
248
+ return {**base, "choices": [{"index": 0, "delta": delta, "finish_reason": fin}], **x}
249
+
250
+ parts = [ch({"role": "assistant", "content": ""})]
251
+ for key in ("reasoning_content", "content"):
252
+ t = msg.get(key) or ""
253
+ for i in range(0, len(t), 10):
254
+ parts.append(ch({key: t[i:i + 10]}))
255
+ for i, c in enumerate(msg.get("tool_calls") or []):
256
+ parts.append(ch({"tool_calls": [{"index": i, "id": c["id"], "type": "function",
257
+ "function": {"name": c["function"]["name"], "arguments": ""}}]}))
258
+ a = c["function"]["arguments"]
259
+ for k in range(0, len(a), 7):
260
+ parts.append(ch({"tool_calls": [{"index": i, "function": {"arguments": a[k:k + 7]}}]}))
261
+ parts.append(ch({}, r["choices"][0]["finish_reason"], usage=r["usage"], timings=r["timings"]))
262
+ self.send_response(200)
263
+ self.send_header("Content-Type", "text/event-stream")
264
+ self.send_header("Connection", "close")
265
+ self.end_headers()
266
+ try:
267
+ for p in parts:
268
+ self.wfile.write(b"data: " + json.dumps(p).encode() + b"\n\n")
269
+ self.wfile.flush()
270
+ if slow:
271
+ time.sleep(slow)
272
+ self.wfile.write(b"data: [DONE]\n\n")
273
+ self.wfile.flush()
274
+ except ConnectionError: # Windows: ConnectionAbortedError (WinError 10053) too
275
+ eng.cancelled += 1
276
+ self.close_connection = True
277
+
278
+ self.srv = ThreadingHTTPServer(("127.0.0.1", port), H)
279
+ threading.Thread(target=self.srv.serve_forever, daemon=True).start()
280
+ return "http://127.0.0.1:%d" % self.srv.server_address[1]
281
+
282
+ def stop(self):
283
+ if self.srv:
284
+ self.srv.shutdown()
285
+ self.srv.server_close()
286
+
287
+
288
+ if __name__ == "__main__":
289
+ # Stand-alone fake engine for smoke tests: python3 -m ctxproxy.fake_engine --port 18095
290
+ import argparse
291
+ ap = argparse.ArgumentParser(description="fake OpenAI-compatible engine (Strata stand-in) for smoke tests")
292
+ ap.add_argument("--port", type=int, default=18095)
293
+ ap.add_argument("--flavor", choices=["strata", "llama", "ds4", "openai"], default="strata",
294
+ help="engine to imitate (llama = llama-server with --slot-save-path)")
295
+ ap.add_argument("--n-ctx", type=int, default=131072)
296
+ ap.add_argument("--no-slot-save", action="store_true", help="llama flavor: as if started without --slot-save-path")
297
+ a = ap.parse_args()
298
+ url = FakeEngine(flavor=a.flavor, n_ctx=a.n_ctx, slot_save=not a.no_slot_save).start(a.port)
299
+ print("[fake-engine] listening on %s" % url, flush=True)
300
+ try:
301
+ while True:
302
+ time.sleep(3600)
303
+ except KeyboardInterrupt:
304
+ pass