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 ADDED
@@ -0,0 +1,32 @@
1
+ """IcyCache — passerelle LLM universelle avec cache local.
2
+
3
+ Le code vit dans `icycache/src/`, avec `config/` et `static/` a cote — exactement
4
+ la disposition du depot (`src/`, `config/`, `static/`). Comme dans le depot, les
5
+ modules s'importent a plat (`import server`, `import icy_core`, ...) : on ajoute
6
+ donc `src/` au `sys.path` pour que les imports fonctionnent a l'identique dans
7
+ les deux modes (checkout source et installation pip).
8
+
9
+ `PROJECT_ROOT` est le repertoire qui contient `config/` et `static/` :
10
+ - mode depot -> la racine du repo
11
+ - mode installe -> le paquet lui-meme (site-packages/icycache)
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ __version__ = "0.2.0"
19
+
20
+ _HERE = Path(__file__).resolve().parent
21
+
22
+ _SRC = _HERE / "src"
23
+ if _SRC.is_dir() and str(_SRC) not in sys.path:
24
+ sys.path.insert(0, str(_SRC))
25
+
26
+ # La racine qui porte config/ et static/.
27
+ if (_HERE / "config").is_dir():
28
+ PROJECT_ROOT = _HERE
29
+ else:
30
+ PROJECT_ROOT = _HERE.parent
31
+
32
+ __all__ = ["__version__", "PROJECT_ROOT"]
icycache/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Permet `python -m icycache ...` en plus du script console `icycache`."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ from icycache.cli import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
icycache/cli.py ADDED
@@ -0,0 +1,171 @@
1
+ """CLI IcyCache.
2
+
3
+ icycache serve # la gateway HTTP (port 8080)
4
+ icycache mcp # la couche MCP (SSE 8081 / HTTP 8084 / stdio)
5
+ icycache doctor # diagnostic : chemins, config, upstreams, base
6
+ icycache paths # ou sont stockees les donnees
7
+ icycache version
8
+
9
+ `serve` et `mcp` relancent les vrais scripts (`src/server.py`, `src/mcp_server.py`)
10
+ dans un sous-processus : la CLI n'ecrit aucune logique de demarrage, elle ne fait
11
+ que choisir le point d'entree et transmettre les arguments.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from . import PROJECT_ROOT, __version__
23
+
24
+ _SRC = PROJECT_ROOT / "src"
25
+
26
+
27
+ def _run(script: str, extra: list[str]) -> int:
28
+ target = _SRC / script
29
+ if not target.exists():
30
+ print(f"[icycache] introuvable: {target}", file=sys.stderr)
31
+ return 2
32
+ cmd = [sys.executable, str(target), *extra]
33
+ print(f"[icycache] {' '.join(cmd)}", file=sys.stderr)
34
+ return subprocess.call(cmd)
35
+
36
+
37
+ def _cmd_serve(args) -> int:
38
+ extra: list[str] = []
39
+ if args.host:
40
+ extra += ["--host", args.host]
41
+ if args.port:
42
+ extra += ["--port", str(args.port)]
43
+ return _run("server.py", extra)
44
+
45
+
46
+ def _cmd_mcp(args) -> int:
47
+ # stdio passe par une entree dediee : l'import de mcp_server.py ecrit sur
48
+ # stdout, qui appartient au JSON-RPC sur ce transport.
49
+ if args.mode == "mcp-stdio":
50
+ return _run("mcp_stdio_entry.py", [])
51
+ return _run("mcp_server.py", ["--mode", args.mode])
52
+
53
+
54
+ def _cmd_paths(args) -> int:
55
+ sys.path.insert(0, str(_SRC))
56
+ from icy_paths import DATA_DIR, IS_SOURCE_CHECKOUT, PROJECT_ROOT as ROOT
57
+
58
+ print(f"mode : {'depot (source)' if IS_SOURCE_CHECKOUT else 'installe (pip)'}")
59
+ print(f"PROJECT_ROOT : {ROOT}")
60
+ print(f"DATA_DIR : {DATA_DIR}")
61
+ print(f"config gateway : {ROOT / 'config' / 'icycache.gateway.json'}")
62
+ print(f"config legacy : {ROOT / 'config' / 'icycache.json'}")
63
+ print(f"base (cache.db) : {DATA_DIR / 'db' / 'cache.db'}")
64
+ print(f"logs : {DATA_DIR / 'logs'}")
65
+ print(f"static : {ROOT / 'static'}")
66
+ return 0
67
+
68
+
69
+ def _cmd_doctor(args) -> int:
70
+ sys.path.insert(0, str(_SRC))
71
+ import icy_paths
72
+
73
+ problems: list[str] = []
74
+
75
+ print("== IcyCache ==")
76
+ print(f"version : {__version__}")
77
+ print(f"python : {sys.executable}")
78
+ print(f"PROJECT_ROOT : {icy_paths.PROJECT_ROOT}")
79
+ print(f"DATA_DIR : {icy_paths.DATA_DIR}")
80
+ print(f"mode : {'depot (source)' if icy_paths.IS_SOURCE_CHECKOUT else 'installe (pip)'}")
81
+
82
+ cfg_path = Path(os.environ.get("ICYCACHE_CONFIG") or (icy_paths.PROJECT_ROOT / "config" / "icycache.gateway.json"))
83
+ print(f"\n== config ==\n{cfg_path}")
84
+ if cfg_path.exists():
85
+ try:
86
+ cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
87
+ except Exception as exc: # noqa: BLE001
88
+ problems.append(f"config illisible: {exc}")
89
+ cfg = {}
90
+ ups = sorted(cfg.get("upstreams", {}))
91
+ gw = cfg.get("gateway", {})
92
+ print(f"upstreams : {len(ups)} -> {', '.join(ups)}")
93
+ print(f"default : {gw.get('default_upstream') or '(aucun - explicite obligatoire)'}")
94
+ print(f"sticky_upstream : {gw.get('sticky_upstream')}")
95
+ cache = cfg.get("cache", {})
96
+ print(f"cache : max_entries={cache.get('max_entries')} "
97
+ f"semantic={cache.get('enable_semantic')} ttl={cache.get('ttl_seconds')}")
98
+ db = icy_paths.resolve_data_path(cache.get("db_path"), "db/cache.db")
99
+ else:
100
+ problems.append(f"config absent: {cfg_path}")
101
+ db = icy_paths.DATA_DIR / "db" / "cache.db"
102
+
103
+ print(f"\n== base ==\n{db}")
104
+ print(f"existe : {db.exists()}"
105
+ + (f" ({db.stat().st_size} octets)" if db.exists() else ""))
106
+ parent = db.parent
107
+ if not parent.exists():
108
+ problems.append(f"repertoire de base absent (sera cree au demarrage): {parent}")
109
+ elif not os.access(parent, os.W_OK):
110
+ problems.append(f"repertoire de base non inscriptible: {parent}")
111
+
112
+ static = icy_paths.PROJECT_ROOT / "static" / "dashboard.html"
113
+ print(f"\n== dashboard ==\n{static}\nexiste : {static.exists()}")
114
+ if not static.exists():
115
+ problems.append(f"dashboard absent: {static}")
116
+
117
+ print()
118
+ if problems:
119
+ print("== PROBLEMES ==")
120
+ for p in problems:
121
+ print(f" - {p}")
122
+ return 1
123
+ print("== OK ==")
124
+ return 0
125
+
126
+
127
+ def _cmd_version(args) -> int:
128
+ print(__version__)
129
+ return 0
130
+
131
+
132
+ def build_parser() -> argparse.ArgumentParser:
133
+ parser = argparse.ArgumentParser(
134
+ prog="icycache",
135
+ description="IcyCache — passerelle LLM universelle avec cache local.",
136
+ )
137
+ sub = parser.add_subparsers(dest="cmd")
138
+
139
+ p = sub.add_parser("serve", help="Demarrer la gateway HTTP (defaut 127.0.0.1:8080).")
140
+ p.add_argument("--host", default=None)
141
+ p.add_argument("--port", type=int, default=None)
142
+ p.set_defaults(func=_cmd_serve)
143
+
144
+ p = sub.add_parser("mcp", help="Demarrer la couche MCP.")
145
+ p.add_argument("--mode", default="mcp-sse",
146
+ choices=["proxy", "mcp-stdio", "mcp-sse", "mcp-http", "all"])
147
+ p.set_defaults(func=_cmd_mcp)
148
+
149
+ p = sub.add_parser("doctor", help="Diagnostic de l'installation.")
150
+ p.set_defaults(func=_cmd_doctor)
151
+
152
+ p = sub.add_parser("paths", help="Afficher ou sont stockees les donnees.")
153
+ p.set_defaults(func=_cmd_paths)
154
+
155
+ p = sub.add_parser("version", help="Afficher la version.")
156
+ p.set_defaults(func=_cmd_version)
157
+
158
+ return parser
159
+
160
+
161
+ def main(argv: list[str] | None = None) -> int:
162
+ parser = build_parser()
163
+ args = parser.parse_args(argv)
164
+ if not getattr(args, "func", None):
165
+ parser.print_help()
166
+ return 0
167
+ return args.func(args)
168
+
169
+
170
+ if __name__ == "__main__": # pragma: no cover
171
+ raise SystemExit(main())
@@ -0,0 +1,111 @@
1
+ {
2
+ "proxy": {
3
+ "host": "127.0.0.1",
4
+ "port": 8080,
5
+ "timeout": 120
6
+ },
7
+ "mcp": {
8
+ "host": "127.0.0.1",
9
+ "port": 8081,
10
+ "transport": ["stdio", "sse"]
11
+ },
12
+ "mcp_gateway": {
13
+ "host": "127.0.0.1",
14
+ "port": 8083,
15
+ "transport": ["stdio", "sse"],
16
+ "_note": "Serveur MCP de la gateway. 8081 est servi par mcp_server.py (serveur MCP)."
17
+ },
18
+ "gateway": {
19
+ "host": "127.0.0.1",
20
+ "port": 8082,
21
+ "default_upstream": "omniroute",
22
+ "ws_delta_interval_s": 0.25,
23
+ "sticky_upstream": true,
24
+ "_note": "default_upstream = defaut EXPLICITE de la config (pas de fallback code vers OmniRoute). null = le client doit nommer son upstream (prefixe /<upstream>/v1/..., header X-IcyCache-Upstream ou champ 'upstream')."
25
+ },
26
+ "upstreams": {
27
+ "omniroute": {"base_url": "http://localhost:20128", "kind": "openai"},
28
+ "lmstudio": {"base_url": "http://localhost:1234", "kind": "openai"},
29
+ "ollama": {"base_url": "http://localhost:11434", "kind": "openai"},
30
+ "openai": {"base_url": "https://api.openai.com", "kind": "openai"},
31
+ "anthropic": {"base_url": "https://api.anthropic.com", "kind": "anthropic"},
32
+ "nvidia": {"base_url": "https://integrate.api.nvidia.com/v1", "kind": "openai"},
33
+ "groq": {"base_url": "https://api.groq.com/openai/v1", "kind": "openai"},
34
+ "cerebras": {"base_url": "https://api.cerebras.ai/v1", "kind": "openai", "headers": {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) HermesAgent/0.22.2"}},
35
+ "openrouter": {"base_url": "https://openrouter.ai/api/v1", "kind": "openai"},
36
+ "kilo": {"base_url": "https://ai-gateway.kilo.ai/api/v1", "kind": "openai"},
37
+ "nous": {"base_url": "https://inference-api.nousresearch.com/v1", "kind": "openai"},
38
+ "copilot": {"base_url": "https://api.githubcopilot.com", "kind": "openai"},
39
+ "ai-gateway": {"base_url": "https://ai-gateway.vercel.sh/v1", "kind": "openai"},
40
+ "huggingface": {"base_url": "https://router.huggingface.co/v1", "kind": "openai"},
41
+ "google-aistudio": {"base_url": "https://generativelanguage.googleapis.com/v1beta/openai", "kind": "openai", "strip_v1": true},
42
+ "llm7io": {"base_url": "https://api.llm7.io/v1", "kind": "openai"},
43
+ "mistral": {"base_url": "https://api.mistral.ai/v1", "kind": "openai"},
44
+ "cloudflare": {"base_url": "https://api.cloudflare.com/client/v4/accounts/dfbe889cfe78700a4d10897cfd3b581d/ai/v1", "kind": "openai", "headers": {"cf-aig-gateway-id": "default"}}
45
+ },
46
+ "cache": {
47
+ "db_path": "db/cache.db",
48
+ "max_entries": 50000,
49
+ "enable_semantic": false,
50
+ "semantic_threshold": 0.95,
51
+ "ttl_seconds": 604800,
52
+ "cache_when_temperature_unset": true,
53
+ "embeddings_ttl_seconds": 604800
54
+ },
55
+ "loop_guard": {
56
+ "enabled": true,
57
+ "window_seconds": 300,
58
+ "tools": {"warn_after": 3, "block_after": 5, "max_cycle_len": 3},
59
+ "llm": {"warn_after": 3, "action": "warn", "block_after": 6}
60
+ },
61
+ "tool_cache": {
62
+ "enabled": true,
63
+ "default": "allow",
64
+ "max_result_bytes": 10485760,
65
+ "downstream": {
66
+ "github": {"transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"]},
67
+ "fetch": {"transport": "stdio", "command": "uvx", "args": ["mcp-server-fetch@2026.10.10"]},
68
+ "omniroute": {"transport": "sse", "url": "http://127.0.0.1:20128/mcp"},
69
+ "qwen-chroma": {"transport": "stdio", "command": "python", "args": ["C:/AI/EmbeddingDB/hermes_qdrant_mcp.py"]},
70
+ "icycache": {"transport": "sse", "url": "http://127.0.0.1:8081/sse"},
71
+ "loom": {"transport": "stdio", "command": "D:/Loom/.venv/Scripts/loom-mcp.exe"},
72
+ "wispr_flow_mcp": {"transport": "stdio", "command": "wispr-flow-mcp"},
73
+ "dak-notebook": {"transport": "stdio", "command": "dak-notebook"},
74
+ "dak-viz": {"transport": "stdio", "command": "dak-viz"},
75
+ "dak-bigquery": {"transport": "stdio", "command": "dak-bigquery"},
76
+ "dak-gcs": {"transport": "stdio", "command": "dak-gcs"},
77
+ "dak-spanner": {"transport": "stdio", "command": "dak-spanner"}
78
+ },
79
+ "policy": {
80
+ "github.*": {"ttl_seconds": 3600},
81
+ "fetch.*": {"ttl_seconds": 3600},
82
+ "omniroute.*": {"ttl_seconds": 1800},
83
+ "qwen-chroma.*": {"ttl_seconds": 7200},
84
+ "icycache.*": {"ttl_seconds": 300},
85
+ "loom.*": {"ttl_seconds": 3600},
86
+ "wispr_flow_mcp.*": {"ttl_seconds": 600},
87
+ "dak-.*": {"ttl_seconds": 1800}
88
+ },
89
+ "_example": {
90
+ "downstream": {"srv1": {"transport": "stdio", "command": "node", "args": ["server.js"]}},
91
+ "policy": {"srv1.nom_du_tool": {"ttl_seconds": 60}}
92
+ }
93
+ },
94
+ "normalization": {
95
+ "remove_politeness": true,
96
+ "canonical_synonyms": {
97
+ "config": "configuration",
98
+ "setup": "configuration",
99
+ "stp": "",
100
+ "s'il te pla\u00eet": "",
101
+ "please": "",
102
+ "can you": "",
103
+ "peux-tu": ""
104
+ }
105
+ },
106
+ "logging": {
107
+ "level": "INFO",
108
+ "file": "logs/app.log",
109
+ "max_size_mb": 10
110
+ }
111
+ }
@@ -0,0 +1,149 @@
1
+ {
2
+ "proxy": {
3
+ "host": "127.0.0.1",
4
+ "port": 8080,
5
+ "target_provider": "http://localhost:20128/v1/chat/completions",
6
+ "api_key": null,
7
+ "timeout": 120
8
+ },
9
+ "mcp": {
10
+ "host": "127.0.0.1",
11
+ "port": 8081,
12
+ "transport": [
13
+ "stdio",
14
+ "sse"
15
+ ],
16
+ "_note": "8081 = transport SSE (clients legacy / Hermes). Le transport moderne streamable-http ecoute sur mcp_http.port."
17
+ },
18
+ "mcp_http": {
19
+ "host": "127.0.0.1",
20
+ "port": 8084,
21
+ "_note": "Streamable HTTP (transport standard des clients MCP recents). Endpoint: http://127.0.0.1:8084/mcp"
22
+ },
23
+ "cache": {
24
+ "db_path": "db/cache.db",
25
+ "max_entries": 200000,
26
+ "enable_semantic": true,
27
+ "semantic_threshold": 0.80,
28
+ "ttl_seconds": 2592000
29
+ },
30
+ "tool_cache": {
31
+ "enabled": true,
32
+ "ttl_seconds": 3600,
33
+ "window_seconds": 1800,
34
+ "tools": {
35
+ "warn_after": 3,
36
+ "block_after": 5,
37
+ "max_cycle_len": 3
38
+ }
39
+ },
40
+ "tool_proxy": {
41
+ "list_cache_seconds": 30,
42
+ "_note": "Jetons de portabilite: {PROJECT_ROOT} = racine du projet, {PYTHON} = interpreteur du venv. Le cache d'outils est OPT-IN; 'cache_tools' = allowlist stricte; sinon 'cache: true' active la detection anti-ecriture (create_/write_/delete_/push_... jamais caches).",
43
+ "servers": {
44
+ "fetch": {
45
+ "command": "uvx",
46
+ "args": ["mcp-server-fetch@2026.10.10"],
47
+ "cache": true
48
+ },
49
+ "github": {
50
+ "command": "npx",
51
+ "args": ["-y", "@modelcontextprotocol/server-github"],
52
+ "env": {
53
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "@hermes:mcp_servers.github"
54
+ },
55
+ "cache": true,
56
+ "cache_tools": {
57
+ "_note": "Allowlist stricte des outils de LECTURE reels de @modelcontextprotocol/server-github (26 outils au total). Les 12 outils d'ecriture (create_*, push_files, fork_repository, merge_*, update_*) ne sont JAMAIS mis en cache: sinon l'ecriture est sautee silencieusement.",
58
+ "search_issues": true,
59
+ "search_code": true,
60
+ "search_repositories": true,
61
+ "search_users": true,
62
+ "get_issue": true,
63
+ "get_pull_request": true,
64
+ "get_pull_request_files": true,
65
+ "get_pull_request_status": true,
66
+ "get_pull_request_comments": true,
67
+ "get_pull_request_reviews": true,
68
+ "get_file_contents": true,
69
+ "list_issues": true,
70
+ "list_pull_requests": true,
71
+ "list_commits": true
72
+ }
73
+ },
74
+ "qwen-chroma": {
75
+ "command": "C:\\AI\\EmbeddingDB\\.venv\\Scripts\\python.exe",
76
+ "args": ["C:\\AI\\EmbeddingDB\\hermes_qdrant_mcp.py"],
77
+ "cache": true,
78
+ "ttl_seconds": 259200,
79
+ "_note": "cache: true + detection anti-ecriture: memory_search_tool / hybrid_search_tool / memory_stats_tool sont caches; memory_add_tool / memory_delete_tool / rebuild_bm25_index_tool sont executes a chaque appel."
80
+ },
81
+ "pwsh": {
82
+ "command": "uvx",
83
+ "args": ["pwsh-exec@1.0.1"],
84
+ "cache": false
85
+ },
86
+ "filesystem": {
87
+ "command": "npx",
88
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "D:\\\\", "C:\\\\Users\\\\Tech\\\\AppData\\\\Local\\\\hermes"],
89
+ "cache": false
90
+ },
91
+ "websearch": {
92
+ "command": "uvx",
93
+ "args": ["mcp-server-brave-search@2026.10.10"],
94
+ "env": {
95
+ "BRAVE_API_KEY": "@hermes:mcp_servers.brave"
96
+ },
97
+ "cache": true
98
+ },
99
+ "sqlite": {
100
+ "command": "uvx",
101
+ "args": ["mcp-server-sqlite@2025.4.25", "--db-path", "D:\\IcyCache\\db\\cache.db"],
102
+ "cache": true
103
+ },
104
+ "docker": {
105
+ "command": "uvx",
106
+ "args": ["mcp-server-docker@0.3.0"],
107
+ "cache": false
108
+ },
109
+ "postgres": {
110
+ "command": "uvx",
111
+ "args": ["mcp-server-postgres@0.1.0"],
112
+ "env": {
113
+ "POSTGRES_CONNECTION_STRING": "@hermes:mcp_servers.postgres"
114
+ },
115
+ "cache": false
116
+ },
117
+ "redis": {
118
+ "command": "uvx",
119
+ "args": ["mcp-server-redis@0.1.1"],
120
+ "env": {
121
+ "REDIS_URL": "@hermes:mcp_servers.redis"
122
+ },
123
+ "cache": true
124
+ },
125
+ "kubernetes": {
126
+ "command": "uvx",
127
+ "args": ["mcp-server-kubernetes@0.1.6"],
128
+ "cache": false
129
+ }
130
+ }
131
+ },
132
+ "normalization": {
133
+ "remove_politeness": true,
134
+ "canonical_synonyms": {
135
+ "config": "configuration",
136
+ "setup": "configuration",
137
+ "stp": "",
138
+ "s'il te pla\u00eet": "",
139
+ "please": "",
140
+ "can you": "",
141
+ "peux-tu": ""
142
+ }
143
+ },
144
+ "logging": {
145
+ "level": "INFO",
146
+ "file": "logs/app.log",
147
+ "max_size_mb": 10
148
+ }
149
+ }