codefreedom 0.1.0__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.
- codefreedom/__init__.py +3 -0
- codefreedom/__main__.py +5 -0
- codefreedom/cli/__init__.py +1 -0
- codefreedom/cli/claude.py +122 -0
- codefreedom/cli/litellm_cli.py +352 -0
- codefreedom/cli/main.py +325 -0
- codefreedom/cli/proxy.py +332 -0
- codefreedom/env_loader.py +101 -0
- codefreedom/examples/.env.example +205 -0
- codefreedom/examples/.env.secrets.example +79 -0
- codefreedom/examples/profiles/claude-code-profiles.json +81 -0
- codefreedom/examples/profiles/claude-code-profiles.schema.json +84 -0
- codefreedom/examples/proxy/config.yaml +112 -0
- codefreedom/examples/proxy/docker-compose.yml +127 -0
- codefreedom/examples/proxy/providers/anthropic-compatible.yaml +50 -0
- codefreedom/examples/proxy/providers/azure-foundry.yaml +76 -0
- codefreedom/examples/proxy/providers/deepseek.yaml +71 -0
- codefreedom/examples/proxy/providers/local.yaml +107 -0
- codefreedom/examples/proxy/providers/nvidia.yaml +103 -0
- codefreedom/examples/proxy/providers/openai-compatible.yaml +57 -0
- codefreedom/examples/proxy/providers/opencode-zen.yaml +87 -0
- codefreedom/launcher.py +389 -0
- codefreedom/profiles.py +200 -0
- codefreedom-0.1.0.dist-info/METADATA +262 -0
- codefreedom-0.1.0.dist-info/RECORD +29 -0
- codefreedom-0.1.0.dist-info/WHEEL +5 -0
- codefreedom-0.1.0.dist-info/entry_points.txt +3 -0
- codefreedom-0.1.0.dist-info/licenses/LICENSE +201 -0
- codefreedom-0.1.0.dist-info/top_level.txt +1 -0
codefreedom/__init__.py
ADDED
codefreedom/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CodeFreedom CLI package."""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Claude Code subcommand — launch Claude Code with profile-based routing.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
codefreedom claude [--profile NAME] [--sandbox] [--stop|--status|--list-profiles] [claude-args...]
|
|
5
|
+
cf cc [same]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from codefreedom.env_loader import eprint, load_env_chain
|
|
14
|
+
from codefreedom.launcher import run_docker, run_local, status, stop
|
|
15
|
+
from codefreedom.profiles import (
|
|
16
|
+
get_profile_sandbox_image,
|
|
17
|
+
list_profiles,
|
|
18
|
+
load_profile_env,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Default location for profiles — ~/.codefreedom/profiles/claude-code.json
|
|
22
|
+
# Can be overridden with CODEFREEDOM_PROFILES_FILE env var
|
|
23
|
+
import os as _os
|
|
24
|
+
|
|
25
|
+
_CODEFREEDOM_DIR = Path.home() / ".codefreedom"
|
|
26
|
+
|
|
27
|
+
DEFAULT_PROFILES_FILE = _os.environ.get(
|
|
28
|
+
"CODEFREEDOM_PROFILES_FILE",
|
|
29
|
+
str(_CODEFREEDOM_DIR / "profiles" / "claude-code.json"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(args: argparse.Namespace) -> int:
|
|
34
|
+
"""Execute the claude subcommand. Returns exit code."""
|
|
35
|
+
|
|
36
|
+
# Fast-path flags (no env loading needed)
|
|
37
|
+
if args.list_profiles:
|
|
38
|
+
profiles_path = _resolve_profiles_path()
|
|
39
|
+
profiles = list_profiles(profiles_path)
|
|
40
|
+
if not profiles:
|
|
41
|
+
eprint("[PROFILES] No profiles found.")
|
|
42
|
+
return 0
|
|
43
|
+
eprint(f"[PROFILES] Available profiles ({profiles_path}):\n")
|
|
44
|
+
for p in profiles:
|
|
45
|
+
override_word = "override" if len(p["env_keys"]) == 1 else "overrides"
|
|
46
|
+
inheritance = (
|
|
47
|
+
"standalone"
|
|
48
|
+
if p["standalone"]
|
|
49
|
+
else f"inherits from 'default' — {len(p['env_keys'])} {override_word}"
|
|
50
|
+
)
|
|
51
|
+
eprint(f" {p['name']}")
|
|
52
|
+
eprint(f" {p['description']}")
|
|
53
|
+
eprint(f" ({inheritance})")
|
|
54
|
+
if p["env_keys"]:
|
|
55
|
+
keys_summary = ", ".join(p["env_keys"][:5])
|
|
56
|
+
if len(p["env_keys"]) > 5:
|
|
57
|
+
keys_summary += ", …"
|
|
58
|
+
eprint(f" sets: {keys_summary}")
|
|
59
|
+
if p.get("sandbox_env_keys"):
|
|
60
|
+
eprint(f" sandbox: {', '.join(p['sandbox_env_keys'])}")
|
|
61
|
+
if p.get("local_env_keys"):
|
|
62
|
+
eprint(f" local: {', '.join(p['local_env_keys'])}")
|
|
63
|
+
eprint()
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
if args.status:
|
|
67
|
+
return status()
|
|
68
|
+
|
|
69
|
+
if args.stop:
|
|
70
|
+
return stop()
|
|
71
|
+
|
|
72
|
+
# ── Load env chain ─────────────────────────────────────────────────────
|
|
73
|
+
workspace_dir = Path.cwd()
|
|
74
|
+
eprint("[ENV] Loading configuration...")
|
|
75
|
+
base_env = load_env_chain(workspace_dir)
|
|
76
|
+
|
|
77
|
+
# ── Load profile ───────────────────────────────────────────────────────
|
|
78
|
+
profile_name = args.profile or "default"
|
|
79
|
+
profiles_path = _resolve_profiles_path()
|
|
80
|
+
|
|
81
|
+
profile_env: dict = {}
|
|
82
|
+
sandbox_image: str | None = None
|
|
83
|
+
mode = "sandbox" if args.sandbox else "local"
|
|
84
|
+
if profiles_path.exists():
|
|
85
|
+
profile_env = load_profile_env(profile_name, profiles_path, base_env, mode)
|
|
86
|
+
sandbox_image = get_profile_sandbox_image(profile_name, profiles_path)
|
|
87
|
+
elif profile_name != "default":
|
|
88
|
+
eprint(
|
|
89
|
+
f"[ERROR] Profile '{profile_name}' requested but no profiles file found."
|
|
90
|
+
)
|
|
91
|
+
return 1
|
|
92
|
+
else:
|
|
93
|
+
eprint("[PROFILE] No profiles file found. Using defaults only.")
|
|
94
|
+
|
|
95
|
+
# ── Route execution ────────────────────────────────────────────────────
|
|
96
|
+
if args.native_models:
|
|
97
|
+
# Strip proxy-auth env vars so Claude Code uses its native /login auth
|
|
98
|
+
_NATIVE_STRIP_VARS = {
|
|
99
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
100
|
+
"ANTHROPIC_BASE_URL",
|
|
101
|
+
"IS_SANDBOX",
|
|
102
|
+
}
|
|
103
|
+
for var in _NATIVE_STRIP_VARS:
|
|
104
|
+
if var in profile_env:
|
|
105
|
+
eprint(
|
|
106
|
+
f"[NATIVE] Stripping '{var}' — using native Anthropic /login auth"
|
|
107
|
+
)
|
|
108
|
+
del profile_env[var]
|
|
109
|
+
|
|
110
|
+
if args.sandbox:
|
|
111
|
+
return run_docker(
|
|
112
|
+
profile_env, args.claude_args, workspace_dir, profile_name, sandbox_image
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
return run_local(profile_env, args.claude_args)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_profiles_path() -> Path:
|
|
119
|
+
"""Return the profiles path (~/.codefreedom/profiles/claude-code.json)."""
|
|
120
|
+
if _os.environ.get("CODEFREEDOM_PROFILES_FILE"):
|
|
121
|
+
return Path(_os.environ["CODEFREEDOM_PROFILES_FILE"])
|
|
122
|
+
return _CODEFREEDOM_DIR / "profiles" / "claude-code.json"
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""LiteLLM subcommand — start, stop, validate, status.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
codefreedom litellm --start Start LiteLLM proxy
|
|
5
|
+
codefreedom litellm --stop Stop LiteLLM proxy
|
|
6
|
+
codefreedom litellm --validate Validate config
|
|
7
|
+
codefreedom litellm --status Show proxy status
|
|
8
|
+
cf ll --start --native Run litellm directly (no Docker)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
from codefreedom.env_loader import eprint
|
|
21
|
+
|
|
22
|
+
# ── Path resolution ──────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
_PACKAGE_DIR = Path(__file__).resolve().parent.parent.parent.parent
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _find_compose_file() -> Optional[Path]:
|
|
28
|
+
"""Find the LiteLLM docker-compose file."""
|
|
29
|
+
candidates = [
|
|
30
|
+
_PACKAGE_DIR / "litellm" / "docker-compose.litellm.yml",
|
|
31
|
+
Path.cwd() / "litellm" / "docker-compose.litellm.yml",
|
|
32
|
+
]
|
|
33
|
+
for c in candidates:
|
|
34
|
+
if c.exists():
|
|
35
|
+
return c
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _find_config_file() -> Optional[Path]:
|
|
40
|
+
"""Find the LiteLLM config.yaml."""
|
|
41
|
+
candidates = [
|
|
42
|
+
_PACKAGE_DIR / "litellm" / "config" / "config.yaml",
|
|
43
|
+
Path.cwd() / "litellm" / "config" / "config.yaml",
|
|
44
|
+
]
|
|
45
|
+
for c in candidates:
|
|
46
|
+
if c.exists():
|
|
47
|
+
return c
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Entry point ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run(args: argparse.Namespace) -> int:
|
|
55
|
+
"""Execute the litellm subcommand. Returns exit code."""
|
|
56
|
+
|
|
57
|
+
if args.start:
|
|
58
|
+
return _start(args)
|
|
59
|
+
elif args.stop:
|
|
60
|
+
return _stop()
|
|
61
|
+
elif args.validate:
|
|
62
|
+
return _validate()
|
|
63
|
+
elif args.status:
|
|
64
|
+
return _status()
|
|
65
|
+
else:
|
|
66
|
+
eprint(
|
|
67
|
+
"[litellm] No action specified. Use --start, --stop, --validate, or --status."
|
|
68
|
+
)
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── Start ────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _start(args: argparse.Namespace) -> int:
|
|
76
|
+
"""Start the LiteLLM proxy."""
|
|
77
|
+
if args.native:
|
|
78
|
+
return _start_native(args)
|
|
79
|
+
else:
|
|
80
|
+
return _start_compose()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _start_compose() -> int:
|
|
84
|
+
"""Start LiteLLM via docker compose."""
|
|
85
|
+
compose_file = _find_compose_file()
|
|
86
|
+
if not compose_file:
|
|
87
|
+
eprint("[ERROR] Could not find litellm/docker-compose.litellm.yml")
|
|
88
|
+
eprint(" Run from the codefreedom project root.")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
eprint(f"[litellm] Starting LiteLLM via Docker Compose ({compose_file})...")
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
[
|
|
94
|
+
"docker",
|
|
95
|
+
"compose",
|
|
96
|
+
"-f",
|
|
97
|
+
str(compose_file),
|
|
98
|
+
"--profile",
|
|
99
|
+
"litellm",
|
|
100
|
+
"up",
|
|
101
|
+
"-d",
|
|
102
|
+
],
|
|
103
|
+
capture_output=False,
|
|
104
|
+
check=False,
|
|
105
|
+
)
|
|
106
|
+
if result.returncode == 0:
|
|
107
|
+
eprint("[litellm] ✓ Proxy started at http://localhost:4000")
|
|
108
|
+
else:
|
|
109
|
+
eprint("[litellm] ✖ Failed to start. Check docker logs.")
|
|
110
|
+
return result.returncode
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _start_native(args: argparse.Namespace) -> int:
|
|
114
|
+
"""Start LiteLLM directly as a Python process."""
|
|
115
|
+
try:
|
|
116
|
+
__import__("litellm")
|
|
117
|
+
except ImportError:
|
|
118
|
+
eprint("[ERROR] litellm package not installed.")
|
|
119
|
+
eprint(" Install: pip install codefreedom[litellm]")
|
|
120
|
+
eprint(" This installs litellm with proxy extras (websockets, etc.).")
|
|
121
|
+
eprint(" Or run without --native to use Docker Compose.")
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
# Find the litellm CLI binary (installed by the litellm package)
|
|
125
|
+
litellm_bin = shutil.which("litellm")
|
|
126
|
+
if not litellm_bin:
|
|
127
|
+
eprint("[ERROR] litellm CLI not found on PATH.")
|
|
128
|
+
eprint(" Ensure litellm is installed: pip install codefreedom[litellm]")
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
config_file = _find_config_file()
|
|
132
|
+
if not config_file:
|
|
133
|
+
eprint("[ERROR] Could not find litellm/config/config.yaml")
|
|
134
|
+
eprint(" Run: codefreedom setup")
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
port = args.port or 4000
|
|
138
|
+
host = args.host or "0.0.0.0"
|
|
139
|
+
|
|
140
|
+
eprint(f"[litellm] Starting natively on {host}:{port}...")
|
|
141
|
+
eprint(f"[litellm] Config: {config_file}")
|
|
142
|
+
|
|
143
|
+
cmd = [
|
|
144
|
+
litellm_bin,
|
|
145
|
+
"--config",
|
|
146
|
+
str(config_file),
|
|
147
|
+
"--port",
|
|
148
|
+
str(port),
|
|
149
|
+
"--host",
|
|
150
|
+
host,
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
proc = subprocess.Popen(cmd)
|
|
155
|
+
eprint(f"[litellm] ✓ Proxy starting (PID: {proc.pid})")
|
|
156
|
+
eprint("[litellm] Press Ctrl+C to stop.")
|
|
157
|
+
proc.wait()
|
|
158
|
+
return proc.returncode
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
eprint("\n[litellm] Proxy stopped.")
|
|
161
|
+
return 0
|
|
162
|
+
except FileNotFoundError:
|
|
163
|
+
eprint("[ERROR] Could not find litellm executable.")
|
|
164
|
+
return 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── Stop ─────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _stop() -> int:
|
|
171
|
+
"""Stop the LiteLLM proxy."""
|
|
172
|
+
compose_file = _find_compose_file()
|
|
173
|
+
if not compose_file:
|
|
174
|
+
eprint("[ERROR] Could not find litellm/docker-compose.litellm.yml")
|
|
175
|
+
return 1
|
|
176
|
+
|
|
177
|
+
eprint("[litellm] Stopping LiteLLM proxy...")
|
|
178
|
+
result = subprocess.run(
|
|
179
|
+
["docker", "compose", "-f", str(compose_file), "--profile", "litellm", "down"],
|
|
180
|
+
capture_output=False,
|
|
181
|
+
check=False,
|
|
182
|
+
)
|
|
183
|
+
if result.returncode == 0:
|
|
184
|
+
eprint("[litellm] ✓ Proxy stopped.")
|
|
185
|
+
return result.returncode
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ── Validate ─────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _validate() -> int:
|
|
192
|
+
"""Validate the LiteLLM configuration."""
|
|
193
|
+
config_file = _find_config_file()
|
|
194
|
+
if not config_file:
|
|
195
|
+
eprint("[ERROR] Could not find litellm/config/config.yaml")
|
|
196
|
+
eprint(" Run: codefreedom setup")
|
|
197
|
+
return 1
|
|
198
|
+
|
|
199
|
+
errors: List[str] = []
|
|
200
|
+
|
|
201
|
+
eprint(f"[litellm] Validating {config_file}...")
|
|
202
|
+
eprint()
|
|
203
|
+
|
|
204
|
+
# Parse config.yaml
|
|
205
|
+
try:
|
|
206
|
+
import yaml
|
|
207
|
+
except ImportError:
|
|
208
|
+
eprint("[WARN] PyYAML not installed. Using basic validation only.")
|
|
209
|
+
eprint(" Install: pip install pyyaml")
|
|
210
|
+
# Fallback: just check file existence
|
|
211
|
+
_validate_basic(config_file, errors)
|
|
212
|
+
_print_validation_result(errors)
|
|
213
|
+
return 0 if not errors else 1
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
with open(config_file, encoding="utf-8") as f:
|
|
217
|
+
config = yaml.safe_load(f)
|
|
218
|
+
except yaml.YAMLError as e:
|
|
219
|
+
eprint(f" ✖ YAML parse error: {e}")
|
|
220
|
+
return 1
|
|
221
|
+
except FileNotFoundError:
|
|
222
|
+
eprint(f" ✖ File not found: {config_file}")
|
|
223
|
+
return 1
|
|
224
|
+
|
|
225
|
+
if not isinstance(config, dict):
|
|
226
|
+
eprint(" ✖ Config must be a YAML dictionary.")
|
|
227
|
+
return 1
|
|
228
|
+
|
|
229
|
+
# Check includes
|
|
230
|
+
includes = config.get("include", [])
|
|
231
|
+
if not includes:
|
|
232
|
+
eprint(" ⚠ No provider includes found in config.yaml")
|
|
233
|
+
eprint(" Run: codefreedom setup")
|
|
234
|
+
else:
|
|
235
|
+
config_dir = config_file.parent
|
|
236
|
+
for inc in includes:
|
|
237
|
+
provider_file = config_dir / inc
|
|
238
|
+
if provider_file.exists():
|
|
239
|
+
eprint(f" ✓ {inc}")
|
|
240
|
+
# Validate provider file
|
|
241
|
+
try:
|
|
242
|
+
with open(provider_file, encoding="utf-8") as f:
|
|
243
|
+
provider_config = yaml.safe_load(f)
|
|
244
|
+
models = provider_config.get("model_list", [])
|
|
245
|
+
for m in models:
|
|
246
|
+
name = m.get("model_name", "?")
|
|
247
|
+
params = m.get("litellm_params", {})
|
|
248
|
+
api_key_ref = params.get("api_key", "")
|
|
249
|
+
# Check if env var is set
|
|
250
|
+
if api_key_ref.startswith("os.environ/"):
|
|
251
|
+
env_var = api_key_ref[len("os.environ/") :]
|
|
252
|
+
if not _env_is_set(env_var):
|
|
253
|
+
eprint(f" ⚠ {name}: env var {env_var} is not set")
|
|
254
|
+
else:
|
|
255
|
+
eprint(f" ✓ {name} (auth: {env_var} ✓)")
|
|
256
|
+
else:
|
|
257
|
+
eprint(f" ✓ {name}")
|
|
258
|
+
except yaml.YAMLError as e:
|
|
259
|
+
eprint(f" ✖ {inc}: YAML error — {e}")
|
|
260
|
+
errors.append(f"YAML error in {inc}: {e}")
|
|
261
|
+
else:
|
|
262
|
+
eprint(f" ✖ {inc} — file not found")
|
|
263
|
+
errors.append(f"Missing provider file: {inc}")
|
|
264
|
+
|
|
265
|
+
# Check essential settings
|
|
266
|
+
general = config.get("general_settings", {})
|
|
267
|
+
if not general.get("database_url"):
|
|
268
|
+
eprint(" ⚠ database_url not set in general_settings")
|
|
269
|
+
|
|
270
|
+
litellm_settings = config.get("litellm_settings", {})
|
|
271
|
+
if not litellm_settings:
|
|
272
|
+
eprint(" ⚠ litellm_settings section is empty")
|
|
273
|
+
|
|
274
|
+
router = config.get("router_settings", {})
|
|
275
|
+
aliases = router.get("model_group_alias", {})
|
|
276
|
+
if aliases:
|
|
277
|
+
eprint(f" ✓ Model aliases: {len(aliases)} defined")
|
|
278
|
+
for alias, model in aliases.items():
|
|
279
|
+
eprint(f" {alias} → {model}")
|
|
280
|
+
else:
|
|
281
|
+
eprint(" ⚠ No model_group_alias defined")
|
|
282
|
+
|
|
283
|
+
eprint()
|
|
284
|
+
_print_validation_result(errors)
|
|
285
|
+
return 0 if not errors else 1
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _validate_basic(config_file: Path, errors: List[str]) -> None:
|
|
289
|
+
"""Basic validation without PyYAML."""
|
|
290
|
+
content = config_file.read_text()
|
|
291
|
+
|
|
292
|
+
checks = [
|
|
293
|
+
("include:", "provider includes"),
|
|
294
|
+
("general_settings:", "general_settings section"),
|
|
295
|
+
("router_settings:", "router_settings section"),
|
|
296
|
+
("litellm_settings:", "litellm_settings section"),
|
|
297
|
+
("model_group_alias:", "model aliases"),
|
|
298
|
+
]
|
|
299
|
+
for marker, label in checks:
|
|
300
|
+
if marker in content:
|
|
301
|
+
eprint(f" ✓ {label} found")
|
|
302
|
+
else:
|
|
303
|
+
eprint(f" ✖ {label} missing")
|
|
304
|
+
errors.append(f"Missing: {label}")
|
|
305
|
+
|
|
306
|
+
# Check provider files exist
|
|
307
|
+
config_dir = config_file.parent
|
|
308
|
+
for line in content.split("\n"):
|
|
309
|
+
line = line.strip()
|
|
310
|
+
if line.startswith("- providers/"):
|
|
311
|
+
provider_file = line[2:].strip()
|
|
312
|
+
full = config_dir / provider_file
|
|
313
|
+
if full.exists():
|
|
314
|
+
eprint(f" ✓ {provider_file}")
|
|
315
|
+
else:
|
|
316
|
+
eprint(f" ✖ {provider_file} — not found")
|
|
317
|
+
errors.append(f"Missing: {provider_file}")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _env_is_set(var_name: str) -> bool:
|
|
321
|
+
"""Check if an environment variable is set and non-empty."""
|
|
322
|
+
import os
|
|
323
|
+
|
|
324
|
+
return bool(os.environ.get(var_name))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _print_validation_result(errors: List[str]) -> None:
|
|
328
|
+
"""Print validation summary."""
|
|
329
|
+
if errors:
|
|
330
|
+
eprint(f" ✖ {len(errors)} issue(s) found.")
|
|
331
|
+
for e in errors:
|
|
332
|
+
eprint(f" - {e}")
|
|
333
|
+
else:
|
|
334
|
+
eprint(" ✓ Configuration looks good!")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ── Status ───────────────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _status() -> int:
|
|
341
|
+
"""Show LiteLLM proxy status."""
|
|
342
|
+
compose_file = _find_compose_file()
|
|
343
|
+
if not compose_file:
|
|
344
|
+
eprint("[ERROR] Could not find litellm/docker-compose.litellm.yml")
|
|
345
|
+
return 1
|
|
346
|
+
|
|
347
|
+
result = subprocess.run(
|
|
348
|
+
["docker", "compose", "-f", str(compose_file), "ps"],
|
|
349
|
+
capture_output=False,
|
|
350
|
+
check=False,
|
|
351
|
+
)
|
|
352
|
+
return result.returncode
|