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/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))