proxytool 2.0.1__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.
- proxytool/__init__.py +108 -0
- proxytool/__main__.py +14 -0
- proxytool/cli.py +617 -0
- proxytool/conduit.py +67 -0
- proxytool/config.py +488 -0
- proxytool/loopguard.py +92 -0
- proxytool/main.py +558 -0
- proxytool/routing.py +106 -0
- proxytool/settings.py +66 -0
- proxytool/trace.py +81 -0
- proxytool-2.0.1.dist-info/METADATA +552 -0
- proxytool-2.0.1.dist-info/RECORD +14 -0
- proxytool-2.0.1.dist-info/WHEEL +4 -0
- proxytool-2.0.1.dist-info/entry_points.txt +3 -0
proxytool/settings.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Proxy settings, driven by LMW_* / LTP_* environment variables + CLI flags."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _model_map() -> dict[str, str]:
|
|
10
|
+
"""Client-facing model id → real upstream model id.
|
|
11
|
+
|
|
12
|
+
Sources: LMW_MODEL_MAP (inline JSON object) or LMW_MODEL_MAP_FILE (path
|
|
13
|
+
to a JSON object). The bundled opencode configs use friendly ids
|
|
14
|
+
(`qwen-gguf`, `qwen-mlx`) that llama-server ignores but mlx_lm.server
|
|
15
|
+
resolves against HF — producing "Repository Not Found". With no mapping
|
|
16
|
+
configured, the alias passes through unchanged.
|
|
17
|
+
"""
|
|
18
|
+
raw = os.environ.get("LTP_MODEL_MAP") or os.environ.get("LMW_MODEL_MAP") or ""
|
|
19
|
+
path = os.environ.get("LTP_MODEL_MAP_FILE") or os.environ.get("LMW_MODEL_MAP_FILE") or ""
|
|
20
|
+
data: dict[str, str] = {}
|
|
21
|
+
if path:
|
|
22
|
+
with open(path, encoding="utf-8") as f:
|
|
23
|
+
data.update(json.load(f))
|
|
24
|
+
elif raw:
|
|
25
|
+
data.update(json.loads(raw))
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _env(name: str, default: str) -> str:
|
|
30
|
+
"""LTP_* wins over the legacy LMW_* spelling."""
|
|
31
|
+
return os.environ.get(f"LTP_{name}") or os.environ.get(f"LMW_{name}", default)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Settings:
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self.host: str = _env("HOST", "127.0.0.1")
|
|
37
|
+
self.port: int = int(_env("PORT", "8180"))
|
|
38
|
+
self.upstream_url: str = _env("UPSTREAM_URL", "http://127.0.0.1:8181")
|
|
39
|
+
# Total seconds the middleware waits on an upstream request. Long
|
|
40
|
+
# model loads / cold-starts (llama-swap spins a backend on demand)
|
|
41
|
+
# need a large read window; SSE reads ride the read timeout too.
|
|
42
|
+
self.upstream_timeout: float = float(_env("UPSTREAM_TIMEOUT", "300"))
|
|
43
|
+
# Optional static bearer token sent as Authorization to the upstream
|
|
44
|
+
# (for llama-swap gateways / llama-server --api-key). Client auth
|
|
45
|
+
# headers are forwarded per-request separately in the conduit.
|
|
46
|
+
self.upstream_api_key: str = _env("UPSTREAM_API_KEY", "")
|
|
47
|
+
self.backend: str = _env("BACKEND", "gguf")
|
|
48
|
+
self.trace: bool = (_env("TRACE", "") == "1" or _env("TRACE_FILE", "") != "")
|
|
49
|
+
self.model_map: dict[str, str] = _model_map()
|
|
50
|
+
# LoopGuard thresholds (0 disables that escape entirely).
|
|
51
|
+
self.loop_nudge: int = int(_env("LOOP_NUDGE", "3") or 0)
|
|
52
|
+
self.loop_abort: int = int(_env("LOOP_ABORT", "6") or 0)
|
|
53
|
+
|
|
54
|
+
def upstream_url_changed_by_flag(self, flag_value: str | None) -> bool:
|
|
55
|
+
"""True when a --upstream-url CLI flag (or its env) already set a URL."""
|
|
56
|
+
return bool(flag_value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_settings() -> Settings:
|
|
60
|
+
"""Build Settings from env.
|
|
61
|
+
|
|
62
|
+
Deliberately does NOT touch logging configuration: the CLI (or an
|
|
63
|
+
explicit ``configure_logging`` call by the embedder) owns that, and
|
|
64
|
+
re-applying an env default here would clobber a CLI ``--log-level``.
|
|
65
|
+
"""
|
|
66
|
+
return Settings()
|
proxytool/trace.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Trace: LMW_TRACE=1 dumps payload transit points to stderr for debugging.
|
|
2
|
+
|
|
3
|
+
Optionally, an app-level logger can be injected (``attach_logger``); when
|
|
4
|
+
present, trace payloads are also emitted at the DEBUG level through it so
|
|
5
|
+
they show up in the configured logging pipeline.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any, TextIO
|
|
15
|
+
|
|
16
|
+
_enabled = False
|
|
17
|
+
_trace_file: TextIO | None = None
|
|
18
|
+
_trace_log: Any = None # set to a real logger via attach_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _NoopLogger:
|
|
22
|
+
"""Silent stand-in used until/independent of an injected logger."""
|
|
23
|
+
|
|
24
|
+
def debug(self, *args: Any, **kwargs: Any) -> None:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def info(self, *args: Any, **kwargs: Any) -> None:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def warning(self, *args: Any, **kwargs: Any) -> None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def error(self, *args: Any, **kwargs: Any) -> None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _noop_logger() -> _NoopLogger:
|
|
38
|
+
return _NoopLogger()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def enable(enabled: bool) -> None:
|
|
42
|
+
global _enabled
|
|
43
|
+
_enabled = enabled
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def enabled() -> bool:
|
|
47
|
+
return _enabled or _trace_file is not None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def attach_logger(logger: Any) -> None:
|
|
51
|
+
"""Inject an app-level logger into the trace dumper.
|
|
52
|
+
|
|
53
|
+
When set, trace payloads are also emitted through the app logger at the
|
|
54
|
+
DEBUG level instead of only the raw stderr sink.
|
|
55
|
+
"""
|
|
56
|
+
global _trace_log
|
|
57
|
+
_trace_log = logger
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# SIM115 is intentional: the trace sink lives for the process lifetime and is
|
|
61
|
+
# cleaned up when the uvicorn process exits; no context manager applies.
|
|
62
|
+
def _get_sink() -> TextIO:
|
|
63
|
+
global _trace_file
|
|
64
|
+
if _trace_file is None:
|
|
65
|
+
path = os.environ.get("LMW_TRACE_FILE")
|
|
66
|
+
sink: TextIO = (
|
|
67
|
+
open(path, "a", encoding="utf-8") if path else sys.stderr # noqa: SIM115
|
|
68
|
+
)
|
|
69
|
+
_trace_file = sink
|
|
70
|
+
return _trace_file
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def log(stage: str, payload: object) -> None:
|
|
74
|
+
if not (enabled() or os.environ.get("LMW_TRACE_FILE")):
|
|
75
|
+
return
|
|
76
|
+
body = f"[{stage}]\n{json.dumps(payload, indent=2, default=str)}\n"
|
|
77
|
+
print(body, file=_get_sink(), flush=True)
|
|
78
|
+
if _trace_log is not None:
|
|
79
|
+
# Logging must never break payload forwarding.
|
|
80
|
+
with contextlib.suppress(Exception):
|
|
81
|
+
_trace_log.debug("trace %s: %s", stage, json.dumps(payload, default=str))
|