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.
ctxproxy/demo.py ADDED
@@ -0,0 +1,193 @@
1
+ # Author: Maurizio Verde — LastReload
2
+ """`mnemonic-proxy demo`: see the proxy work in under a minute, offline, with no model and no GPU.
3
+
4
+ A scripted fake model (ctxproxy.fake_engine) runs behind the real proxy code in a temporary folder:
5
+
6
+ 1. an agent session reads a long build log that contains one identifier;
7
+ 2. the session keeps growing until the proxy hides old tool outputs (masking);
8
+ 3. we show what the engine actually receives in place of the log (a one-line receipt);
9
+ 4. the user asks for the identifier: the model calls `recall`, the proxy resolves it internally, the client only
10
+ sees the final answer;
11
+ 5. the archived text returned by `recall` is compared byte for byte with the original output.
12
+
13
+ What it proves: archive, masking, recall (the proxy's own logic). What it does not prove: the quality of a real
14
+ model, tool calling of a real model, saved engine state (KV) — that needs a real engine (README, Tier 1).
15
+ Exit code 0 = PASS, 1 = FAIL.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import re
22
+ import sys
23
+ import tempfile
24
+ import time
25
+
26
+ from .core import RECEIPT_OPEN, OUT_PREFIX, Config, Journal, Store, TokenCounter
27
+ from .fake_engine import FakeEngine
28
+ from .server import Proxy
29
+ from .upstream import Upstream
30
+
31
+ SECRET = "BUILD-7f3a9c-ORCHID-42"
32
+ TOOLS = [{"type": "function", "function": {
33
+ "name": "bash", "description": "run a shell command",
34
+ "parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}}]
35
+ SYSTEM = {"role": "system", "content": "You are a coding agent. Use the tools to inspect the project."}
36
+ QUESTION = "What was the build id printed in build.log?"
37
+
38
+
39
+ def build_log() -> str:
40
+ lines = []
41
+ for i in range(1, 181):
42
+ lines.append("[%04d] compiling module_%03d.c ... ok (%d ms)" % (i, i, 40 + (i * 37) % 300))
43
+ if i == 97:
44
+ lines.append("[%04d] build id: %s (keep this for the release notes)" % (i, SECRET))
45
+ return "\n".join(lines) + "\n"
46
+
47
+
48
+ def filler(k: int) -> str:
49
+ return "".join("test_%02d_%03d ... passed in %d.%02ds\n" % (k, j, j % 3, (j * 7) % 100) for j in range(150))
50
+
51
+
52
+ def call(cid: str, cmd: str) -> dict:
53
+ return {"role": "assistant", "content": "", "tool_calls": [
54
+ {"id": cid, "type": "function", "function": {"name": "bash", "arguments": json.dumps({"command": cmd})}}]}
55
+
56
+
57
+ class ScriptedModel:
58
+ """The fake model. It never sees anything the proxy did not send it."""
59
+
60
+ def __init__(self):
61
+ self.recall_args = None
62
+
63
+ def __call__(self, body: dict) -> dict:
64
+ msgs = body.get("messages") or []
65
+ last = msgs[-1] if msgs else {}
66
+ if last.get("role") == "tool" and str(last.get("content", "")).startswith("[recall"):
67
+ m = re.search(r"build id: (\S+)", last["content"])
68
+ ans = ("The build id was %s (found in the archived build.log)." % m.group(1)) if m else \
69
+ "I could not find it."
70
+ return {"role": "assistant", "content": ans}
71
+ if last.get("role") == "user" and last.get("content") == QUESTION:
72
+ # the log is no longer in the prompt: only the receipt with its id is. Ask the proxy for the exact text.
73
+ for m in msgs:
74
+ c = str(m.get("content") or "")
75
+ if m.get("role") == "tool" and (c.startswith(RECEIPT_OPEN) or c.startswith(OUT_PREFIX)) \
76
+ and "cat build.log" in c:
77
+ rid = re.search(r"id=(\w+)", c)
78
+ if rid is None:
79
+ continue
80
+ self.recall_args = {"id": rid.group(1)}
81
+ return {"role": "assistant", "content": "", "tool_calls": [
82
+ {"id": "call_recall", "type": "function",
83
+ "function": {"name": "recall", "arguments": json.dumps(self.recall_args)}}]}
84
+ if any(SECRET in str(m.get("content")) for m in msgs):
85
+ return {"role": "assistant", "content": "The build id was %s (still in my prompt)." % SECRET}
86
+ return {"role": "assistant", "content": "I do not know."}
87
+ return {"role": "assistant", "content": "ok"}
88
+
89
+
90
+ def _p(out, s=""):
91
+ print(s, file=out, flush=True)
92
+
93
+
94
+ def run(out=None, keep: str | None = None) -> bool:
95
+ out = out or sys.stdout
96
+ t0 = time.time()
97
+ checks: list[tuple[str, bool]] = []
98
+ _p(out, "mnemonic-proxy demo — offline, scripted fake model, temporary folder\n")
99
+ _p(out, "Proves: verbatim archive, masking (receipts), recall resolved inside the proxy.")
100
+ _p(out, "Does not prove: real model quality or tool calling, saved engine state (KV). See README, Tier 1.\n")
101
+ tmp = tempfile.TemporaryDirectory(prefix="mnemonic-demo-", ignore_cleanup_errors=True)
102
+ data = keep or tmp.name
103
+ os.makedirs(data, exist_ok=True)
104
+ model = ScriptedModel()
105
+ eng = FakeEngine(model, flavor="openai")
106
+ url = eng.start(0)
107
+ store = None
108
+ try:
109
+ cfg = Config.from_dict({"window": 16384, "mask_trigger": 6000, "mask_target": 3000,
110
+ "keep_recent_tokens": 1500, "min_batch_tokens": 1000, "min_age_turns": 2,
111
+ "reserve": 512, "default_response": 1024, "tail_max": 3000,
112
+ "notes_max_tokens": 1024, "recall_max_tokens": 6000})
113
+ store = Store(os.path.join(data, "archive.sqlite"))
114
+ proxy = Proxy(cfg, Upstream(url), store, Journal(os.path.join(data, "journal.jsonl")), TokenCounter(3.5))
115
+ log = build_log()
116
+ h = [SYSTEM, {"role": "user", "content": "The release build failed. Check build.log, then run the tests."},
117
+ call("call_log", "cat build.log"), {"role": "tool", "tool_call_id": "call_log", "content": log}]
118
+
119
+ def send(msgs):
120
+ st, r, _ = proxy.chat({"model": "local-model", "messages": msgs, "tools": TOOLS, "max_tokens": 512})
121
+ if st != 200:
122
+ raise RuntimeError("proxy returned HTTP %d: %s" % (st, r))
123
+ return r
124
+
125
+ _p(out, "1. The agent reads build.log: %d lines, %d chars; line 98 holds the build id %s."
126
+ % (log.count("\n"), len(log), SECRET))
127
+ masked_at = None
128
+ for k in range(1, 13):
129
+ r = send(h)
130
+ ctx = r["strata_context"]
131
+ if ctx["masked"] and masked_at is None:
132
+ masked_at = k
133
+ _p(out, "2. Turn %d: the session holds ~%d tokens; the proxy hid %d old output(s) (~%d tokens)."
134
+ % (k, ctx["virtual_tokens"], ctx["masked"], ctx["masked_tokens"]))
135
+ break
136
+ h += [call("call_t%d" % k, "pytest -q tests/part%d" % k),
137
+ {"role": "tool", "tool_call_id": "call_t%d" % k, "content": filler(k)}]
138
+ checks.append(("masking happened", masked_at is not None))
139
+ sent = eng.requests[-1]["messages"]
140
+ seen = next((m for m in sent if m.get("role") == "tool" and m.get("tool_call_id") == "call_log"), None)
141
+ hidden = seen is not None and SECRET not in str(seen.get("content")) and len(str(seen.get("content"))) < 600
142
+ checks.append(("the engine no longer receives build.log", hidden))
143
+ _p(out, "3. What the engine receives in place of build.log (%d chars instead of %d):"
144
+ % (len(str((seen or {}).get("content"))), len(log)))
145
+ _p(out, " " + str((seen or {}).get("content"))[:400].replace("\n", " "))
146
+ _p(out, " (receipts are in English for new conversations; conversations born before 0.3.1 keep Italian)")
147
+ n0 = len(eng.requests)
148
+ h += [{"role": "assistant", "content": "Tests pass."}, {"role": "user", "content": QUESTION}]
149
+ r = send(h)
150
+ answer = r["choices"][0]["message"].get("content") or ""
151
+ internal = len(eng.requests) - n0
152
+ _p(out, "4. The user asks: %r" % QUESTION)
153
+ _p(out, " The model called recall(%s); the proxy answered it itself (%d engine rounds, the client saw one"
154
+ " reply):" % (json.dumps(model.recall_args), internal))
155
+ _p(out, " " + answer)
156
+ checks.append(("recall resolved inside the proxy", model.recall_args is not None and internal == 2
157
+ and not r["choices"][0]["message"].get("tool_calls")))
158
+ checks.append(("answer contains the build id", SECRET in answer))
159
+ conv = r["strata_context"]["conversation_id"]
160
+ got = proxy.mgr.recall(conv, model.recall_args or {})
161
+ body = got.split("\n", 1)[1] if "\n" in got else ""
162
+ same = body == log
163
+ checks.append(("recall returns the original output byte for byte", same))
164
+ _p(out, "5. recall(%s) returns %d chars; identical to the original: %s"
165
+ % (json.dumps(model.recall_args), len(body), "yes" if same else "NO"))
166
+ except Exception as e: # noqa: BLE001
167
+ checks.append(("demo ran without errors: %r" % e, False))
168
+ finally:
169
+ eng.stop()
170
+ if store is not None:
171
+ store.db.close()
172
+ tmp.cleanup()
173
+ ok = bool(checks) and all(c for _, c in checks)
174
+ _p(out)
175
+ for name, c in checks:
176
+ _p(out, " [%s] %s" % ("ok" if c else "FAIL", name))
177
+ _p(out, "\n%s in %.1f s" % ("PASS" if ok else "FAIL", time.time() - t0))
178
+ if ok:
179
+ _p(out, "Next: a real model on CPU — README, Tier 1 (`mnemonic-proxy check` verifies the setup).")
180
+ return ok
181
+
182
+
183
+ def main(argv=None):
184
+ import argparse
185
+ ap = argparse.ArgumentParser(prog="mnemonic-proxy demo",
186
+ description="Offline demo: archive, masking and recall with a scripted fake model.")
187
+ ap.add_argument("--keep", metavar="DIR", help="keep the demo archive in DIR instead of a temporary folder")
188
+ a = ap.parse_args(argv)
189
+ sys.exit(0 if run(keep=a.keep) else 1)
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()
ctxproxy/engines.py ADDED
@@ -0,0 +1,314 @@
1
+ # Author: Maurizio Verde — LastReload
2
+ """Motori dietro al proxy: che cosa sa fare ciascuno (docs/dev-notes/ENGINES-RESULT.md).
3
+
4
+ Il proxy è nato davanti a Strata; qui si descrive il motore e lo si riconosce all'avvio:
5
+
6
+ kind strata | llama.cpp | openai (generico OpenAI-compatibile, anche online)
7
+ slot_save salvataggio/ripristino della sessione su file (POST /slots/{id}?action=save|restore)
8
+ slot_id slot usato (llama-server con --parallel N: il proxy fissa `id_slot` nelle richieste)
9
+ status stato del motore normalizzato nella forma di /v1/status di Strata:
10
+ {"loaded", "started", "activity": {"requests", "in_flight"}}
11
+ - Strata: /v1/status così com'è;
12
+ - llama-server: /slots (id_task dello slot = contatore richieste, is_processing = in corso);
13
+ tokenize conteggio token esatto lato motore (POST /tokenize di llama-server)
14
+ n_ctx finestra di contesto (da /props o /slots; Strata: dalla configurazione)
15
+
16
+ ds4-server (https://github.com/antirez/ds4): niente /v1/status, /props, /slots, /tokenize; si riconosce da
17
+ `GET /v1/models`, i cui modelli hanno `owned_by: "ds4.c"` e `context_length` (= --ctx). Salva da solo lo stato su
18
+ disco (`--kv-disk-dir`, checkpoint per prefisso di testo) e ricorda il testo esatto delle tool call per id
19
+ («exact DSML tool replay»): il proxy quindi non salva né archivia stato (slot_save spento) e non accorcia gli
20
+ argomenti delle chiamate (`mask_tool_args`), perché ds4 rimetterebbe nel prompt il testo originale preso per id.
21
+
22
+ Rilevamento (`detect`): /v1/status con `activity` -> Strata; /v1/models con `owned_by` "ds4.c" -> ds4;
23
+ /props con `default_generation_settings` ->
24
+ llama.cpp (salvataggi attivi se il server è partito con --slot-save-path: lo si prova con un'azione non valida,
25
+ che risponde 400 "Invalid action" se i salvataggi ci sono e 501 se mancano, senza toccare lo slot); altrimenti
26
+ OpenAI generico. `engine` in configurazione forza il tipo (auto = rileva).
27
+
28
+ `apply` spegne con un avviso le funzioni che richiedono i salvataggi quando il motore non li ha
29
+ (slot_save, mask_anchor, autosave/autorestore, seal_experimental, kv_archive) e, con llama.cpp, prende la finestra
30
+ da n_ctx se non è stata scritta in configurazione (scalando le soglie di serie, pensate per 128K).
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import dataclasses
35
+ import json
36
+ import os
37
+
38
+ KINDS = ("strata", "llama.cpp", "ds4", "openai")
39
+ ALIASES = {"llama": "llama.cpp", "llamacpp": "llama.cpp", "llama-server": "llama.cpp", "llama_cpp": "llama.cpp",
40
+ "ds4-server": "ds4", "ds4.c": "ds4", "dwarfstar": "ds4",
41
+ "generic": "openai", "generic-openai": "openai", "openai-compatible": "openai", "online": "openai"}
42
+ # funzioni che vivono di file di sessione del motore
43
+ NEEDS_SLOTS = ("slot_save", "mask_anchor", "autosave", "autorestore", "seal_experimental", "kv_archive")
44
+ # soglie di serie pensate per la finestra di Strata (131072): con una finestra più piccola si scalano (se non
45
+ # scritte in configurazione)
46
+ SCALED = ("mask_trigger", "mask_target", "keep_recent_tokens", "min_batch_tokens", "tail_max", "reserve",
47
+ "default_response", "notes_max_tokens", "anchor_min_tokens", "autosave_min_tokens", "autorestore_min_gain",
48
+ "pins_max_tokens", "recall_max_tokens", "recall_struct_max_tokens", "auto_recall_max_tokens",
49
+ "response_floor")
50
+ BASE_WINDOW = 131072
51
+ # floors when scaling down (small windows): below these a recall result or a handoff note is too short to be useful
52
+ SCALED_MIN = {"recall_max_tokens": 1024, "recall_struct_max_tokens": 1024, "notes_max_tokens": 1024,
53
+ "default_response": 1024, "auto_recall_max_tokens": 512, "pins_max_tokens": 512}
54
+ # 0.3.0: finestra minima del motore. Sotto 16K il proxy non parte (8K provato dal vivo una sola volta, dopo la
55
+ # correzione della recall: escluso finché non è provato su più corse). Consigliata >= 32K.
56
+ MIN_WINDOW = 16384
57
+ RECOMMENDED_WINDOW = 32768
58
+ # solo per sviluppo (test interni, prove su finestre piccole): non documentata nel README
59
+ ALLOW_SMALL_WINDOW_ENV = "MNEMONIC_PROXY_ALLOW_SMALL_WINDOW"
60
+
61
+
62
+ def small_window_allowed() -> bool:
63
+ return os.environ.get(ALLOW_SMALL_WINDOW_ENV, "").strip().lower() in ("1", "true", "yes")
64
+
65
+
66
+ ENGINE_FIX = ("restart the engine with a bigger window, e.g. `llama-server ... -c %d` or `ds4-server ... --ctx %d`"
67
+ % (RECOMMENDED_WINDOW, RECOMMENDED_WINDOW))
68
+
69
+
70
+ def window_problems(cfg, engine: Engine | None, explicit=()) -> list[tuple[str, str]]:
71
+ """Finestre sotto il minimo -> [(evidence, fix)]: `window` scritta in configurazione e n_ctx del motore.
72
+ Vuota se tutto va bene o se MNEMONIC_PROXY_ALLOW_SMALL_WINDOW=1."""
73
+ if small_window_allowed():
74
+ return []
75
+ out = []
76
+ w = int(getattr(cfg, "window", 0) or 0)
77
+ if "window" in set(explicit or ()) and w and w < MIN_WINDOW:
78
+ out.append(("config window=%d: the minimum is %d tokens, recommended %d or more"
79
+ % (w, MIN_WINDOW, RECOMMENDED_WINDOW),
80
+ "remove \"window\" from the config (it is read from the engine) or set it to %d or more; "
81
+ "the engine window must be at least as big: %s" % (MIN_WINDOW, ENGINE_FIX)))
82
+ n = int(getattr(engine, "n_ctx", 0) or 0) if engine is not None else 0
83
+ if n and n < MIN_WINDOW:
84
+ out.append(("engine window n_ctx=%d: the minimum is %d tokens, recommended %d or more"
85
+ % (n, MIN_WINDOW, RECOMMENDED_WINDOW), ENGINE_FIX))
86
+ return out
87
+
88
+
89
+ @dataclasses.dataclass
90
+ class Engine:
91
+ kind: str = "strata"
92
+ slot_save: bool = True
93
+ slot_id: int = 0
94
+ status_kind: str | None = "strata" # strata | llama.cpp | None
95
+ tokenize: bool = False
96
+ n_ctx: int | None = None
97
+ model: str | None = None
98
+ detected: bool = False # True = rilevato dagli endpoint, False = forzato/di serie
99
+ notes: list = dataclasses.field(default_factory=list)
100
+
101
+ # il contatore richieste di llama-server (id_task) cresce di più di 1 per richiesta: va riletto dopo
102
+ @property
103
+ def counter_after_call(self) -> bool:
104
+ return self.status_kind == "llama.cpp"
105
+
106
+ def summary(self) -> dict:
107
+ return {"kind": self.kind, "slot_save": self.slot_save, "slot_id": self.slot_id, "status": self.status_kind,
108
+ "tokenize": self.tokenize, "n_ctx": self.n_ctx, "model": self.model, "detected": self.detected,
109
+ "notes": self.notes}
110
+
111
+
112
+ def norm_kind(k: str | None) -> str:
113
+ k = (k or "auto").strip().lower()
114
+ k = ALIASES.get(k, k)
115
+ if k not in KINDS + ("auto",):
116
+ raise ValueError("engine sconosciuto %r (strata | llama.cpp | openai | auto)" % k)
117
+ return k
118
+
119
+
120
+ def _get_json(up, path):
121
+ try:
122
+ st, _, data = up.raw("GET", path)
123
+ if st != 200:
124
+ return None
125
+ return json.loads(data)
126
+ except Exception: # noqa: BLE001
127
+ return None
128
+
129
+
130
+ def _slots_enabled(up, slot_id: int) -> bool | None:
131
+ """llama-server: prova un'azione non valida. 400 'Invalid action' = salvataggi attivi; 501 = server senza
132
+ --slot-save-path. Lo slot non viene toccato. None = non si sa."""
133
+ try:
134
+ st, _, data = up.raw("POST", "/slots/%d?action=ctxproxy-probe" % slot_id, b"{}")
135
+ except Exception: # noqa: BLE001
136
+ return None
137
+ if st == 501:
138
+ return False
139
+ if st == 400 and b"action" in data.lower():
140
+ return True
141
+ if st in (404, 405):
142
+ return False
143
+ return None
144
+
145
+
146
+ def detect(up, forced: str | None = "auto", slot_id: int = 0) -> Engine:
147
+ """Riconosce il motore dietro `up` (Upstream). Con `forced` diverso da auto prova solo quel tipo, ma legge
148
+ comunque n_ctx/salvataggi se gli endpoint rispondono."""
149
+ kind = norm_kind(forced)
150
+ s = _get_json(up, "/v1/status") if kind in ("auto", "strata") else None
151
+ if kind == "strata" or (kind == "auto" and isinstance(s, dict) and ("activity" in s or s.get("service") ==
152
+ "strata")):
153
+ e = Engine("strata", slot_save=True, slot_id=slot_id, status_kind="strata", detected=kind == "auto")
154
+ if s is None:
155
+ e.notes.append("/v1/status non risponde (motore spento o non raggiungibile)")
156
+ else:
157
+ # Strata espone la finestra in context (native = max_positions = --max-context): la finestra
158
+ # del motore viene letta qui, come llama.cpp la legge da /slots
159
+ ctx = s.get("context")
160
+ if isinstance(ctx, dict):
161
+ e.n_ctx = int(ctx.get("native") or ctx.get("max_positions") or 0) or None
162
+ return e
163
+ models = _get_json(up, "/v1/models") if kind in ("auto", "ds4") else None
164
+ ds4_models = [m for m in (models or {}).get("data") or [] if isinstance(m, dict)
165
+ and m.get("owned_by") == "ds4.c"] if isinstance(models, dict) else []
166
+ if kind == "ds4" or (kind == "auto" and ds4_models):
167
+ e = Engine("ds4", slot_save=False, slot_id=slot_id, status_kind=None, detected=kind == "auto")
168
+ if ds4_models:
169
+ m0 = ds4_models[0]
170
+ e.model = m0.get("name") or m0.get("id")
171
+ e.n_ctx = int(m0.get("context_length") or 0) or None
172
+ else:
173
+ e.notes.append("/v1/models non risponde come ds4-server (motore spento o non raggiungibile)")
174
+ return e
175
+ props = _get_json(up, "/props") if kind in ("auto", "llama.cpp") else None
176
+ if kind == "llama.cpp" or (kind == "auto" and isinstance(props, dict) and "default_generation_settings" in props):
177
+ e = Engine("llama.cpp", slot_save=False, slot_id=slot_id, status_kind="llama.cpp", tokenize=True,
178
+ detected=kind == "auto")
179
+ props = props or {}
180
+ e.model = os.path.basename(str(props.get("model_path") or "")) or None
181
+ slots = _get_json(up, "/slots")
182
+ if isinstance(slots, list):
183
+ mine = next((x for x in slots if x.get("id") == slot_id), None)
184
+ if mine is None:
185
+ e.notes.append("slot %d assente (slot: %s)" % (slot_id, [x.get("id") for x in slots]))
186
+ else:
187
+ e.n_ctx = int(mine.get("n_ctx") or 0) or None
188
+ if len(slots) > 1:
189
+ e.notes.append("%d slot (--parallel): il proxy fissa id_slot=%d nelle richieste" % (len(slots),
190
+ slot_id))
191
+ else:
192
+ e.status_kind = None
193
+ e.notes.append("/slots non disponibile (server con --no-slots): niente stato del motore")
194
+ if e.n_ctx is None:
195
+ dgs = props.get("default_generation_settings") or {}
196
+ e.n_ctx = int(dgs.get("n_ctx") or 0) or None
197
+ en = _slots_enabled(up, slot_id)
198
+ e.slot_save = bool(en) and e.status_kind is not None
199
+ if en is False:
200
+ e.notes.append("salvataggi spenti: avviare llama-server con --slot-save-path DIR")
201
+ return e
202
+ return Engine("openai", slot_save=False, slot_id=slot_id, status_kind=None, detected=kind == "auto")
203
+
204
+
205
+ def normalize_status(engine: Engine | None, up) -> dict | None:
206
+ """Stato del motore nella forma di /v1/status di Strata (la usano Tracker e Archiver)."""
207
+ if engine is None or engine.status_kind == "strata":
208
+ return _get_json(up, "/v1/status")
209
+ if engine.status_kind == "llama.cpp":
210
+ slots = _get_json(up, "/slots")
211
+ if not isinstance(slots, list):
212
+ return None
213
+ mine = next((x for x in slots if x.get("id") == engine.slot_id), None)
214
+ if mine is None:
215
+ return None
216
+ return {"service": "llama.cpp", "loaded": True, "started": None,
217
+ "activity": {"requests": mine.get("id_task"), "in_flight": int(bool(mine.get("is_processing"))),
218
+ "n_prompt_tokens": mine.get("n_prompt_tokens")}}
219
+ return None
220
+
221
+
222
+ def apply(cfg, engine: Engine, journal=None, explicit: set | None = None, log=print) -> list[str]:
223
+ """Adatta la configurazione al motore: spegne le funzioni senza supporto, finestra da n_ctx. -> avvisi."""
224
+ explicit = set(explicit or ())
225
+ warn = []
226
+ if not engine.slot_save:
227
+ off = [f for f in NEEDS_SLOTS if getattr(cfg, f, False) and (f != "autorestore" or cfg.autosave)]
228
+ for f in NEEDS_SLOTS:
229
+ if getattr(cfg, f, False):
230
+ setattr(cfg, f, False)
231
+ if off:
232
+ warn.append("engine %s has no saved-state support: disabled %s" % (engine.kind, ", ".join(off)))
233
+ if engine.kind == "ds4" and getattr(cfg, "mask_tool_args", False) and getattr(cfg, "ds4_exact_tool_replay", True):
234
+ # ds4 rimette nel prompt il testo campionato della chiamata, preso per id: gli argomenti accorciati dal
235
+ # proxy non arriverebbero al modello (nessun risparmio) ma il proxy li conterebbe come risparmiati
236
+ cfg.mask_tool_args = False
237
+ warn.append("engine ds4 replays sampled tool calls by id: disabled mask_tool_args (set "
238
+ "ds4_exact_tool_replay=false only if ds4-server runs with --disable-exact-dsml-tool-replay)")
239
+ if engine.n_ctx and "window" not in explicit and engine.kind in ("llama.cpp", "ds4", "strata") \
240
+ and engine.n_ctx != cfg.window:
241
+ ratio = engine.n_ctx / float(cfg.window or BASE_WINDOW)
242
+ old = cfg.window
243
+ cfg.window = engine.n_ctx
244
+ scaled = {}
245
+ for f in SCALED:
246
+ if f in explicit or not hasattr(cfg, f):
247
+ continue
248
+ v = getattr(cfg, f)
249
+ if isinstance(v, int) and v > 0:
250
+ nv = max(64, SCALED_MIN.get(f, 0) if ratio < 1 else 0, int(v * ratio))
251
+ nv = min(nv, v) if ratio < 1 else nv
252
+ if nv != v:
253
+ setattr(cfg, f, nv)
254
+ scaled[f] = nv
255
+ warn.append("window from engine n_ctx: %d (was %d); thresholds scaled x%.3f" % (cfg.window, old, ratio))
256
+ if journal is not None:
257
+ journal.log("engine_window", window=cfg.window, was=old, scaled=scaled)
258
+ # una risposta minima che da sola riempie (quasi) la finestra fa scattare un cambio di segmento a ogni richiesta
259
+ floor = int(getattr(cfg, "response_floor", 0) or 0)
260
+ if floor and cfg.window and floor > cfg.window // 4:
261
+ cfg.response_floor = cfg.window // 4
262
+ warn.append("response_floor %d too large for window %d: lowered to %d" % (floor, cfg.window,
263
+ cfg.response_floor))
264
+ for w in warn:
265
+ if log:
266
+ log("[mnemonic-proxy] warning: " + w)
267
+ if journal is not None:
268
+ journal.log("engine_warning", message=w)
269
+ if journal is not None:
270
+ journal.log("engine", **engine.summary())
271
+ return warn
272
+
273
+
274
+ def setup(cfg, up, journal=None, explicit=None, counter=None, log=print) -> Engine:
275
+ """All'avvio del server: rileva (o forza) il motore, lo collega all'Upstream, adatta la configurazione e, se
276
+ chiesto e possibile, usa /tokenize del motore per contare i token."""
277
+ eng = detect(up, getattr(cfg, "engine", "auto"), int(getattr(cfg, "slot_id", 0) or 0))
278
+ if hasattr(up, "set_engine"):
279
+ up.set_engine(eng, getattr(cfg, "slot_dir", "") or "")
280
+ apply(cfg, eng, journal, explicit, log)
281
+ if counter is not None and getattr(cfg, "engine_tokenize", False) and eng.tokenize and not counter.exact:
282
+ counter.tk = RemoteTokenizer(up)
283
+ counter.kind = "esatto:/tokenize"
284
+ return eng
285
+
286
+
287
+ class RemoteTokenizer:
288
+ """Tokenizer via POST /tokenize di llama-server, con l'interfaccia minima di `tokenizers` usata da
289
+ TokenCounter (encode(text, add_special_tokens=False).ids). TokenCounter mette in cache i conteggi."""
290
+
291
+ class _Enc:
292
+ def __init__(self, ids):
293
+ self.ids = ids
294
+
295
+ def __init__(self, up):
296
+ self.up = up
297
+
298
+ def encode(self, text, add_special_tokens=False):
299
+ st, _, data = self.up.raw("POST", "/tokenize", json.dumps({"content": text, "add_special": bool(
300
+ add_special_tokens)}, ensure_ascii=False).encode("utf-8"))
301
+ if st != 200:
302
+ raise RuntimeError("tokenize HTTP %d" % st)
303
+ return self._Enc(json.loads(data).get("tokens") or [])
304
+
305
+
306
+ def slot_error_status(engine: Engine | None, action: str, status: int, body: bytes, slot_dir: str = "",
307
+ filename: str = "") -> int:
308
+ """llama-server risponde 400 anche per un file mancante: lo si riporta a 404 (come Strata) se il file non c'è
309
+ nella slot_dir, così autorestore lo segna come cancellato."""
310
+ if engine is None or engine.kind != "llama.cpp" or action != "restore" or status != 400:
311
+ return status
312
+ if slot_dir and filename and not os.path.exists(os.path.join(slot_dir, filename)):
313
+ return 404
314
+ return status