icycache 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.
- icycache/__init__.py +32 -0
- icycache/__main__.py +9 -0
- icycache/cli.py +171 -0
- icycache/config/icycache.gateway.json +111 -0
- icycache/config/icycache.json +149 -0
- icycache/config/normalization_rules.json +5832 -0
- icycache/config/semantic_cache.json +8 -0
- icycache/config/tools_registry.json +10 -0
- icycache/src/icy_cache_store.py +217 -0
- icycache/src/icy_core.py +380 -0
- icycache/src/icy_normalize.py +197 -0
- icycache/src/icy_paths.py +76 -0
- icycache/src/icy_semantic.py +197 -0
- icycache/src/mcp_gateway.py +360 -0
- icycache/src/mcp_server.py +1319 -0
- icycache/src/mcp_stdio_bridge.py +83 -0
- icycache/src/mcp_stdio_entry.py +23 -0
- icycache/src/prefix_proxy.py +336 -0
- icycache/src/server.py +1142 -0
- icycache/src/tool_proxy.py +655 -0
- icycache/src/upstream_client.py +98 -0
- icycache/src/upstream_utils.py +135 -0
- icycache/static/dashboard.html +2385 -0
- icycache/static/icywebgl.js +2190 -0
- icycache/static/logo.svg +58 -0
- icycache/static/three.min.js +6 -0
- icycache-0.3.2.dist-info/METADATA +422 -0
- icycache-0.3.2.dist-info/RECORD +31 -0
- icycache-0.3.2.dist-info/WHEEL +4 -0
- icycache-0.3.2.dist-info/entry_points.txt +2 -0
- icycache-0.3.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
IcyCache - facade MCP de la gateway.
|
|
4
|
+
|
|
5
|
+
Le MCP est une FACADE : il observe et pilote, il n'est PAS dans le chemin des
|
|
6
|
+
requetes LLM (un client MCP appelle volontairement ses outils, on ne peut pas
|
|
7
|
+
intercepter un flux OpenAI/Anthropic avec lui). Le chemin des requetes passe
|
|
8
|
+
par la gateway HTTP (server_new.py).
|
|
9
|
+
|
|
10
|
+
Outils exposes :
|
|
11
|
+
get_cache_stats etat du cache LLM
|
|
12
|
+
clear_cache purge (confirmation explicite requise)
|
|
13
|
+
normalize_preview apercu de la normalisation (et de la cle de cache)
|
|
14
|
+
search_cached_prompts recherche dans le cache
|
|
15
|
+
get_realtime_tokens DERNIERS tokens REELS par upstream (jamais estimes)
|
|
16
|
+
list_upstreams upstreams disponibles dans la gateway
|
|
17
|
+
tool_cache_stats etat du cache d'outils
|
|
18
|
+
|
|
19
|
+
Surfaces :
|
|
20
|
+
MCP : stdio (--mode mcp-stdio) ou SSE (--mode mcp-sse)
|
|
21
|
+
REST : /api/tools, /api/tools/{nom}, /api/agent/events, /api/loop/check,
|
|
22
|
+
/api/toolcache/{stats,flush,clear}
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sqlite3
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Dict, List, Optional
|
|
35
|
+
|
|
36
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
37
|
+
|
|
38
|
+
from icy_paths import PROJECT_ROOT, resolve_data_path # noqa: E402
|
|
39
|
+
|
|
40
|
+
_CONFIG_FILE = Path(os.environ.get("ICYCACHE_CONFIG") or (PROJECT_ROOT / "config" / "icycache.gateway.json"))
|
|
41
|
+
os.environ["ICYCACHE_CONFIG"] = str(_CONFIG_FILE)
|
|
42
|
+
|
|
43
|
+
from fastapi import FastAPI, HTTPException, Request # noqa: E402
|
|
44
|
+
from fastapi.responses import JSONResponse # noqa: E402
|
|
45
|
+
from mcp.server.fastmcp import FastMCP # noqa: E402
|
|
46
|
+
import uvicorn # noqa: E402
|
|
47
|
+
|
|
48
|
+
import icy_core # noqa: E402
|
|
49
|
+
from icy_core import log_agent_event, utc_iso # noqa: E402
|
|
50
|
+
from icy_cache_store import CacheStore # noqa: E402
|
|
51
|
+
import icy_normalize # noqa: E402
|
|
52
|
+
|
|
53
|
+
# Proxy d'outils: disponible si le module s'importe. IcyCache sert alors aussi
|
|
54
|
+
# de relais vers les serveurs MCP cibles (tools calling).
|
|
55
|
+
try:
|
|
56
|
+
import tool_proxy as tp
|
|
57
|
+
TOOL_PROXY_OK = True
|
|
58
|
+
except Exception: # pragma: no cover
|
|
59
|
+
tp = None
|
|
60
|
+
TOOL_PROXY_OK = False
|
|
61
|
+
|
|
62
|
+
CONFIG: Dict[str, Any] = json.loads(_CONFIG_FILE.read_text(encoding="utf-8"))
|
|
63
|
+
DB_FILE = resolve_data_path(
|
|
64
|
+
os.environ.get("ICYCACHE_DB") or CONFIG.get("cache", {}).get("db_path"),
|
|
65
|
+
"db/cache.db",
|
|
66
|
+
)
|
|
67
|
+
DB_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
UPSTREAMS: Dict[str, Any] = CONFIG.get("upstreams", {})
|
|
70
|
+
MCP_CFG = CONFIG.get("mcp", {})
|
|
71
|
+
MCP_GW_CFG = CONFIG.get("mcp_gateway", {"host": "127.0.0.1", "port": 8083})
|
|
72
|
+
GW_CFG = CONFIG.get("gateway", {})
|
|
73
|
+
LOOP_CFG = CONFIG.get("loop_guard", {})
|
|
74
|
+
TOOL_CACHE_CFG = CONFIG.get("tool_cache", {})
|
|
75
|
+
|
|
76
|
+
CACHE = CacheStore(DB_FILE,
|
|
77
|
+
ttl_seconds=int(CONFIG.get("cache", {}).get("ttl_seconds", 86400)),
|
|
78
|
+
max_entries=int(CONFIG.get("cache", {}).get("max_entries", 10000)))
|
|
79
|
+
LOOP_GUARD = icy_core.ToolLoopGuard(LOOP_CFG)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# =============================================================================
|
|
83
|
+
# OUTILS MCP
|
|
84
|
+
# =============================================================================
|
|
85
|
+
mcp = FastMCP("IcyCache Gateway",
|
|
86
|
+
host=str(MCP_GW_CFG.get("host", "127.0.0.1")),
|
|
87
|
+
port=int(MCP_GW_CFG.get("port", 8083)))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@mcp.tool()
|
|
91
|
+
def get_cache_stats() -> Dict[str, Any]:
|
|
92
|
+
"""Etat du cache LLM (entrees, hits, repartition par upstream/modele)."""
|
|
93
|
+
return CACHE.stats()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def clear_cache(confirm: str) -> str:
|
|
98
|
+
"""Vide le cache. Necessite confirm='FORCE_DELETE'."""
|
|
99
|
+
if confirm != "FORCE_DELETE":
|
|
100
|
+
return "ECHEC: utilisez confirm='FORCE_DELETE' pour valider la suppression destructive."
|
|
101
|
+
n = CACHE.clear()
|
|
102
|
+
return f"SUCCES: cache vide ({n} entrees supprimees)."
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def normalize_preview(text: str) -> Dict[str, str]:
|
|
107
|
+
"""Apercu de la normalisation d'un prompt et de sa cle de cache."""
|
|
108
|
+
norm = icy_normalize.get_default_normalizer()
|
|
109
|
+
normalized = norm.normalize(text)
|
|
110
|
+
return {"original": text, "normalized": normalized, "hash": norm.get_hash(normalized)}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@mcp.tool()
|
|
114
|
+
def search_cached_prompts(query: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
115
|
+
"""Recherche dans les prompts normalises du cache."""
|
|
116
|
+
return CACHE.search(query, limit)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.tool()
|
|
120
|
+
def get_realtime_tokens(limit: int = 20, upstream: str = "") -> Dict[str, Any]:
|
|
121
|
+
"""Derniers tokens REELS constatees en amont (jamais estimes).
|
|
122
|
+
|
|
123
|
+
Retourne pour chaque evenement : upstream, modele, prompt/completion/cached tokens,
|
|
124
|
+
latence, ttft et la source du token ('upstream' ou 'n/a' si l'upstream n'envoie pas d'usage).
|
|
125
|
+
"""
|
|
126
|
+
limit = max(1, min(int(limit), 200))
|
|
127
|
+
conn = sqlite3.connect(DB_FILE)
|
|
128
|
+
conn.row_factory = sqlite3.Row
|
|
129
|
+
try:
|
|
130
|
+
q = ("SELECT id, ts, kind, upstream, client, model_req, model_real, stream, prompt_tokens, "
|
|
131
|
+
"completion_tokens, cached_tokens, cache_write_tokens, latency_ms, ttft_ms, saved_prompt, "
|
|
132
|
+
"saved_completion FROM events")
|
|
133
|
+
args: List[Any] = []
|
|
134
|
+
if upstream:
|
|
135
|
+
q += " WHERE upstream = ?"
|
|
136
|
+
args.append(upstream)
|
|
137
|
+
q += " ORDER BY id DESC LIMIT ?"
|
|
138
|
+
args.append(limit)
|
|
139
|
+
rows = [dict(r) for r in conn.execute(q, args).fetchall()]
|
|
140
|
+
finally:
|
|
141
|
+
conn.close()
|
|
142
|
+
events = []
|
|
143
|
+
for r in rows:
|
|
144
|
+
events.append({
|
|
145
|
+
"id": r["id"], "ts": r["ts"], "kind": r["kind"], "upstream": r["upstream"],
|
|
146
|
+
"client": r["client"], "model": r["model_real"] or r["model_req"], "stream": bool(r["stream"]),
|
|
147
|
+
"prompt_tokens": r["prompt_tokens"], "completion_tokens": r["completion_tokens"],
|
|
148
|
+
"cached_tokens": r["cached_tokens"], "cache_write_tokens": r["cache_write_tokens"],
|
|
149
|
+
"saved_tokens": (r["saved_prompt"] or 0) + (r["saved_completion"] or 0),
|
|
150
|
+
"latency_ms": r["latency_ms"], "ttft_ms": r["ttft_ms"],
|
|
151
|
+
"tokens_source": "upstream" if (r["prompt_tokens"] is not None or r["completion_tokens"] is not None) else "n/a",
|
|
152
|
+
})
|
|
153
|
+
return {
|
|
154
|
+
"events": events,
|
|
155
|
+
"note": ("prompt_tokens / completion_tokens proviennent du champ usage de la reponse upstream. "
|
|
156
|
+
"Valeur NULL = l'upstream n'a pas envoye d'usage : aucune estimation n'est faite."),
|
|
157
|
+
"websocket": f"ws://{GW_CFG.get('host', '127.0.0.1')}:{GW_CFG.get('port', 8082)}/ws/events",
|
|
158
|
+
"generated_at": utc_iso(),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@mcp.tool()
|
|
163
|
+
def list_upstreams() -> Dict[str, Any]:
|
|
164
|
+
"""Upstreams configures dans la gateway (base_url, protocole, defaut)."""
|
|
165
|
+
return {
|
|
166
|
+
"upstreams": {k: {"base_url": v.get("base_url"), "kind": v.get("kind", "openai")}
|
|
167
|
+
for k, v in UPSTREAMS.items()},
|
|
168
|
+
"default_upstream": GW_CFG.get("default_upstream"),
|
|
169
|
+
"note": "Aucun defaut implicite : si default_upstream est absent, le client doit nommer son upstream.",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@mcp.tool()
|
|
174
|
+
def tool_cache_stats() -> Dict[str, Any]:
|
|
175
|
+
"""Etat du cache d'outils (entrees, hits, octets) et evenements agent recents."""
|
|
176
|
+
return {"tool_cache": icy_core.tc_stats(), "recent_agent_events": icy_core.agent_events_recent(limit=20)}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# =============================================================================
|
|
180
|
+
# OUTILS DE RELAY MCP (tool calling vers les serveurs cibles)
|
|
181
|
+
# =============================================================================
|
|
182
|
+
@mcp.tool()
|
|
183
|
+
async def proxy_tool(server: str, tool: str, args: Optional[Dict[str, Any]] = None,
|
|
184
|
+
client: str = "gateway") -> Dict[str, Any]:
|
|
185
|
+
"""Appelle un outil d'un serveur MCP cible via IcyCache (cache + garde anti-boucle).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
server: nom du serveur cible (voir list_proxy_servers)
|
|
189
|
+
tool: nom de l'outil a executer
|
|
190
|
+
args: arguments JSON de l'outil, optionnel
|
|
191
|
+
client: identifiant de l'appelant (session agent), optionnel
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
{"ok": true, "cached": false, "latency_ms": 12, "result": ...}
|
|
195
|
+
{"ok": false, "error": "..."} en cas d'erreur (jamais de faux succes)
|
|
196
|
+
"""
|
|
197
|
+
if not TOOL_PROXY_OK:
|
|
198
|
+
return {"ok": False, "error": "tool_proxy indisponible"}
|
|
199
|
+
return await tp.call_tool(server, tool, args, client)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@mcp.tool()
|
|
203
|
+
async def proxy_tools(calls: List[Dict[str, Any]], client: str = "gateway") -> List[Dict[str, Any]]:
|
|
204
|
+
"""Appel batch d'outils MCP cibles.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
calls: liste de [{"server": "..", "tool": "..", "args": {...}}, ...]
|
|
208
|
+
client: identifiant de l'appelant, optionnel
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
liste des resultats dans le meme ordre (format identique a proxy_tool)
|
|
212
|
+
"""
|
|
213
|
+
if not TOOL_PROXY_OK:
|
|
214
|
+
return [{"ok": False, "error": "tool_proxy indisponible"}]
|
|
215
|
+
return await tp.batch_call(calls or [], client)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@mcp.tool()
|
|
219
|
+
def list_proxy_servers() -> Dict[str, Any]:
|
|
220
|
+
"""Liste des serveurs MCP routables via IcyCache."""
|
|
221
|
+
if not TOOL_PROXY_OK:
|
|
222
|
+
return {"ok": False, "error": "tool_proxy indisponible"}
|
|
223
|
+
names = tp.server_names()
|
|
224
|
+
return {"ok": True, "servers": names, "count": len(names)}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@mcp.tool()
|
|
228
|
+
async def list_proxy_tools(server: str) -> Dict[str, Any]:
|
|
229
|
+
"""Liste les outils exposes par un serveur MCP cible (ex: 'github', 'fetch')."""
|
|
230
|
+
if not TOOL_PROXY_OK:
|
|
231
|
+
return {"ok": False, "error": "tool_proxy indisponible"}
|
|
232
|
+
return await tp.list_server_tools(server)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# =============================================================================
|
|
236
|
+
# API REST (miroir des outils + introspection)
|
|
237
|
+
# =============================================================================
|
|
238
|
+
app = FastAPI(title="IcyCache Gateway MCP", version="2.0.0")
|
|
239
|
+
|
|
240
|
+
TOOL_IMPLS = {
|
|
241
|
+
"get_cache_stats": get_cache_stats,
|
|
242
|
+
"clear_cache": clear_cache,
|
|
243
|
+
"normalize_preview": normalize_preview,
|
|
244
|
+
"search_cached_prompts": search_cached_prompts,
|
|
245
|
+
"get_realtime_tokens": get_realtime_tokens,
|
|
246
|
+
"list_upstreams": list_upstreams,
|
|
247
|
+
"tool_cache_stats": tool_cache_stats,
|
|
248
|
+
"proxy_tool": proxy_tool,
|
|
249
|
+
"proxy_tools": proxy_tools,
|
|
250
|
+
"list_proxy_servers": list_proxy_servers,
|
|
251
|
+
"list_proxy_tools": list_proxy_tools,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@app.get("/health")
|
|
256
|
+
async def health():
|
|
257
|
+
return {"status": "healthy", "tools": sorted(TOOL_IMPLS), "upstreams": sorted(UPSTREAMS)}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@app.get("/api/tools")
|
|
261
|
+
async def list_tools():
|
|
262
|
+
return {"tools": sorted(TOOL_IMPLS), "protocol": "icycache.mcp/v1"}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.post("/api/tools/{name}")
|
|
266
|
+
async def call_tool(name: str, request: Request):
|
|
267
|
+
if name not in TOOL_IMPLS:
|
|
268
|
+
raise HTTPException(status_code=404, detail={"error": "unknown_tool", "tools": sorted(TOOL_IMPLS)})
|
|
269
|
+
try:
|
|
270
|
+
args = await request.json()
|
|
271
|
+
except Exception:
|
|
272
|
+
args = {}
|
|
273
|
+
if not isinstance(args, dict):
|
|
274
|
+
args = {}
|
|
275
|
+
func = TOOL_IMPLS[name]
|
|
276
|
+
t0 = time.perf_counter()
|
|
277
|
+
try:
|
|
278
|
+
if asyncio.iscoroutinefunction(func):
|
|
279
|
+
result = await func(**args)
|
|
280
|
+
else:
|
|
281
|
+
result = await asyncio.to_thread(func, **args)
|
|
282
|
+
except TypeError as exc:
|
|
283
|
+
raise HTTPException(status_code=400, detail={"error": "bad_args", "message": str(exc)})
|
|
284
|
+
latency = int((time.perf_counter() - t0) * 1000)
|
|
285
|
+
log_agent_event("tool_call", server="gateway", tool=name, client="rest",
|
|
286
|
+
args_hash=icy_core.short_hash(json.dumps(args, sort_keys=True, default=str)),
|
|
287
|
+
latency_ms=latency, detail={"args": sorted(args.keys())})
|
|
288
|
+
return {"tool": name, "result": result, "latency_ms": latency}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@app.get("/api/agent/events")
|
|
292
|
+
async def agent_events(limit: int = 50, kind: str = ""):
|
|
293
|
+
kinds = [k for k in (kind or "").split(",") if k] or None
|
|
294
|
+
return {"events": icy_core.agent_events_recent(limit=limit, kinds=kinds)}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@app.post("/api/loop/check")
|
|
298
|
+
async def loop_check(request: Request):
|
|
299
|
+
"""Detecte une boucle d'agent dans l'historique d'une requete LLM.
|
|
300
|
+
|
|
301
|
+
Body : {"messages": [...], "client": "hermes"}
|
|
302
|
+
"""
|
|
303
|
+
body = await request.json()
|
|
304
|
+
messages = body.get("messages") or []
|
|
305
|
+
client = body.get("client") or "unknown"
|
|
306
|
+
warn_after = int(LOOP_CFG.get("llm", {}).get("warn_after", 3))
|
|
307
|
+
verdict = icy_core.llm_loop_check(messages, warn_after=warn_after)
|
|
308
|
+
if verdict:
|
|
309
|
+
log_agent_event("loop_detected", server="gateway", client=client, detail=verdict)
|
|
310
|
+
return {"loop": verdict, "action": LOOP_CFG.get("llm", {}).get("action", "warn"),
|
|
311
|
+
"checked_at": utc_iso()}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.post("/api/toolcache/flush")
|
|
315
|
+
async def toolcache_flush(request: Request):
|
|
316
|
+
body = await request.json()
|
|
317
|
+
server = body.get("server")
|
|
318
|
+
n = icy_core.tc_flush(server) if server else icy_core.tc_clear()
|
|
319
|
+
return {"flushed": n, "server": server}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.get("/api/toolcache/stats")
|
|
323
|
+
async def toolcache_stats():
|
|
324
|
+
return icy_core.tc_stats()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.get("/api/realtime")
|
|
328
|
+
async def realtime(limit: int = 20, upstream: str = ""):
|
|
329
|
+
return get_realtime_tokens(limit=limit, upstream=upstream)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# =============================================================================
|
|
333
|
+
if __name__ == "__main__":
|
|
334
|
+
import argparse
|
|
335
|
+
import logging
|
|
336
|
+
|
|
337
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - IcyCache.mcp - %(levelname)s - %(message)s")
|
|
338
|
+
parser = argparse.ArgumentParser(description="IcyCache Gateway MCP")
|
|
339
|
+
parser.add_argument("--mode", choices=["api", "mcp-stdio", "mcp-sse", "mcp-http", "all"],
|
|
340
|
+
default="api")
|
|
341
|
+
parser.add_argument("--port", type=int, default=int(GW_CFG.get("port", 8082)))
|
|
342
|
+
parser.add_argument("--host", default=str(GW_CFG.get("host", "127.0.0.1")))
|
|
343
|
+
args = parser.parse_args()
|
|
344
|
+
|
|
345
|
+
if args.mode == "mcp-stdio":
|
|
346
|
+
mcp.run(transport="stdio")
|
|
347
|
+
elif args.mode == "mcp-sse":
|
|
348
|
+
mcp.run(transport="sse")
|
|
349
|
+
elif args.mode == "mcp-http":
|
|
350
|
+
# Streamable HTTP: transport standard des clients MCP recents.
|
|
351
|
+
mcp.run(transport="streamable-http")
|
|
352
|
+
elif args.mode == "all":
|
|
353
|
+
threading = __import__("threading")
|
|
354
|
+
t = threading.Thread(target=lambda: uvicorn.run(app, host=args.host, port=args.port,
|
|
355
|
+
log_level="warning"), daemon=True)
|
|
356
|
+
t.start()
|
|
357
|
+
mcp.run(transport="sse")
|
|
358
|
+
else:
|
|
359
|
+
print(f"IcyCache MCP API -> http://{args.host}:{args.port}")
|
|
360
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
|