客户端入口与配置边界 深度解析
拆解 CLI launcher 如何接管 Claude Code/Codex/OpenCode 等客户端的 base URL 和认证变量。
导入链与契约
▸入口契约
多客户端入口通过 ClientSpec 表达。sources/my-free-code/my_free_code/cli/launchers.py:12 到 sources/my-free-code/my_free_code/cli/launchers.py:22 注册 Claude Code、Codex、OpenCode、Cline、Pi、Hermes 等可启动客户端,sources/my-free-code/my_free_code/cli/launchers.py:24 到 sources/my-free-code/my_free_code/cli/launchers.py:39 统一注入本地 base URL、token 和 NO_PROXY。
5@dataclass(frozen=True) 6class ClientSpec: 7 name: str 8 executable: str 9 base_env: str 10 args: tuple[str, ...] = () 11 12CLIENTS = { 13 "claude": ClientSpec("Claude Code", "claude", "ANTHROPIC_BASE_URL"), 14 "codex": ClientSpec("Codex", "codex", "OPENAI_BASE_URL"), 15 "opencode": ClientSpec("OpenCode", "opencode", "OPENAI_BASE_URL"), 16 "cline": ClientSpec("Cline", "cline", "OPENAI_BASE_URL"), 17 "pi": ClientSpec("Pi", "pi", "ANTHROPIC_BASE_URL"), 18 "hermes": ClientSpec("Hermes", "hermes", "OPENAI_BASE_URL"), 19 "deepseek-harness": ClientSpec("DeepSeek Harness", "deepseek-harness", "OPENAI_BASE_URL"), 20 "grok": ClientSpec("Grok Build", "grok", "OPENAI_BASE_URL"), 21 "muse": ClientSpec("Muse Code", "muse", "OPENAI_BASE_URL"), 22} 23 24def launch(client: str, args=None): 25 spec = CLIENTS[client] 26 exe = shutil.which(spec.executable) 27 if not exe: 28 raise SystemExit(f"{spec.executable!r} was not found on PATH.") 29 env = os.environ.copy() 30 base = f"http://{settings.host}:{settings.port}" 31 env["ANTHROPIC_BASE_URL"] = base 32 env["OPENAI_BASE_URL"] = base + "/v1" 33 env["ANTHROPIC_AUTH_TOKEN"] = settings.auth_token 34 env["OPENAI_API_KEY"] = settings.auth_token 35 env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1" 36 no_proxy = env.get("NO_PROXY", "") 37 env["NO_PROXY"] = ",".join(x for x in [no_proxy, "127.0.0.1", "localhost", "::1"] if x) 38 env["no_proxy"] = env["NO_PROXY"] 39 return subprocess.call([exe, *(args or [])], env=env)
核心实现,逐段拆
▸Claude Code 专用 launcher
Claude 专用入口更窄,只要求系统里存在 claude 可执行文件。sources/my-free-code/my_free_code/cli/claude.py:7 到 sources/my-free-code/my_free_code/cli/claude.py:9 做 PATH 检查,sources/my-free-code/my_free_code/cli/claude.py:10 到 sources/my-free-code/my_free_code/cli/claude.py:14 设置 ANTHROPIC_BASE_URL、AUTH_TOKEN 和模型发现变量后透传 argv。
1import os 2import shutil 3import subprocess 4from ..config import settings 5 6def main(): 7 claude = shutil.which("claude") 8 if not claude: 9 raise SystemExit("Claude Code CLI was not found on PATH.") 10 env = os.environ.copy() 11 env["ANTHROPIC_BASE_URL"] = f"http://{settings.host}:{settings.port}" 12 env["ANTHROPIC_AUTH_TOKEN"] = settings.auth_token 13 env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1" 14 raise SystemExit(subprocess.call([claude, *os.sys.argv[1:]], env=env)) 15 16if __name__ == "__main__": 17 main()
▸配置默认值与密钥读取
配置对象是 frozen dataclass,sources/my-free-code/my_free_code/config.py:9 到 sources/my-free-code/my_free_code/config.py:23 从环境变量读取 host、port、auth token、默认模型、fallback 和 provider 限流参数;sources/my-free-code/my_free_code/config.py:25 到 sources/my-free-code/my_free_code/config.py:26 延迟读取 provider key,让 catalog 可以只存 env var 名称。
7@dataclass(frozen=True) 8class Settings: 9 host: str = os.getenv("HOST", "127.0.0.1") 10 port: int = int(os.getenv("PORT", "8082")) 11 auth_token: str = os.getenv("PROXY_AUTH_TOKEN", "local") 12 model: str = os.getenv("MODEL", "open_router/openrouter/free") 13 fable: str = os.getenv("MODEL_FABLE", "") 14 opus: str = os.getenv("MODEL_OPUS", "") 15 sonnet: str = os.getenv("MODEL_SONNET", "") 16 haiku: str = os.getenv("MODEL_HAIKU", "") 17 fallbacks: list[str] = field(default_factory=lambda: [ 18 x.strip() for x in os.getenv("FALLBACK_MODELS", "").split(",") if x.strip() 19 ]) 20 max_concurrency: int = int(os.getenv("PROVIDER_MAX_CONCURRENCY", "5")) 21 rate_limit: int = int(os.getenv("PROVIDER_RATE_LIMIT", "10")) 22 rate_window: float = float(os.getenv("PROVIDER_RATE_WINDOW", "3")) 23 http_timeout: float = float(os.getenv("HTTP_TIMEOUT", "180")) 24 25 def key(self, env_name: str | None) -> str: 26 return os.getenv(env_name, "").strip() if env_name else ""
关键流程
包入口和脚本声明要一起看:sources/my-free-code/pyproject.toml:19 到 sources/my-free-code/pyproject.toml:22 暴露 my-free-code、mfc-claude 和 mfc-client,sources/my-free-code/my_free_code/__main__.py:1 到 sources/my-free-code/my_free_code/__main__.py:7 在 module execution 时先触发 point 同步再启动 main。这里也暴露了 sources/my-free-code/my_free_code/cli/mfc.py:3 的语法风险。
边界 / 失败模式
sources/my-free-code/my_free_code/cli/mfc.py:3是无效 Python import,mfc-client 这条入口需要修复后才能作为稳定产品路径。sources/my-free-code/my_free_code/config.py:11默认 token 是 local,适合单机开发,不适合多人共享代理。sources/my-free-code/my_free_code/cli/launchers.py:36到sources/my-free-code/my_free_code/cli/launchers.py:38自动设置 NO_PROXY,能避免本地代理被系统代理劫持,但也要求用户理解网络路径。