modelmux-cli 0.1.0__tar.gz

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.
Files changed (63) hide show
  1. modelmux_cli-0.1.0/.gitignore +29 -0
  2. modelmux_cli-0.1.0/PKG-INFO +40 -0
  3. modelmux_cli-0.1.0/README.md +23 -0
  4. modelmux_cli-0.1.0/hatch_build.py +33 -0
  5. modelmux_cli-0.1.0/pyproject.toml +88 -0
  6. modelmux_cli-0.1.0/shared/providers/claude.json +34 -0
  7. modelmux_cli-0.1.0/shared/providers/codex.json +35 -0
  8. modelmux_cli-0.1.0/shared/release.json +4 -0
  9. modelmux_cli-0.1.0/shared/schema/provider.schema.json +81 -0
  10. modelmux_cli-0.1.0/shared/templates/compose-service.json +12 -0
  11. modelmux_cli-0.1.0/shared/templates/config/curl.json +8 -0
  12. modelmux_cli-0.1.0/shared/templates/config/env.json +8 -0
  13. modelmux_cli-0.1.0/shared/templates/config/langchain.json +8 -0
  14. modelmux_cli-0.1.0/shared/templates/config/litellm.json +8 -0
  15. modelmux_cli-0.1.0/shared/templates/config/openai-python.json +8 -0
  16. modelmux_cli-0.1.0/src/modelmux_cli/__init__.py +8 -0
  17. modelmux_cli-0.1.0/src/modelmux_cli/__main__.py +7 -0
  18. modelmux_cli-0.1.0/src/modelmux_cli/clientconfig.py +87 -0
  19. modelmux_cli-0.1.0/src/modelmux_cli/commands/__init__.py +1 -0
  20. modelmux_cli-0.1.0/src/modelmux_cli/commands/basic.py +75 -0
  21. modelmux_cli-0.1.0/src/modelmux_cli/commands/config_cmd.py +65 -0
  22. modelmux_cli-0.1.0/src/modelmux_cli/commands/doctor.py +190 -0
  23. modelmux_cli-0.1.0/src/modelmux_cli/commands/login.py +190 -0
  24. modelmux_cli-0.1.0/src/modelmux_cli/commands/logout.py +53 -0
  25. modelmux_cli-0.1.0/src/modelmux_cli/commands/up.py +157 -0
  26. modelmux_cli-0.1.0/src/modelmux_cli/commands/upgrade.py +50 -0
  27. modelmux_cli-0.1.0/src/modelmux_cli/config.py +78 -0
  28. modelmux_cli-0.1.0/src/modelmux_cli/console.py +112 -0
  29. modelmux_cli-0.1.0/src/modelmux_cli/context.py +41 -0
  30. modelmux_cli-0.1.0/src/modelmux_cli/docker.py +340 -0
  31. modelmux_cli-0.1.0/src/modelmux_cli/errors.py +35 -0
  32. modelmux_cli-0.1.0/src/modelmux_cli/files.py +42 -0
  33. modelmux_cli-0.1.0/src/modelmux_cli/health.py +60 -0
  34. modelmux_cli-0.1.0/src/modelmux_cli/main.py +119 -0
  35. modelmux_cli-0.1.0/src/modelmux_cli/paths.py +51 -0
  36. modelmux_cli-0.1.0/src/modelmux_cli/ports.py +29 -0
  37. modelmux_cli-0.1.0/src/modelmux_cli/providers.py +168 -0
  38. modelmux_cli-0.1.0/src/modelmux_cli/py.typed +0 -0
  39. modelmux_cli-0.1.0/src/modelmux_cli/redact.py +35 -0
  40. modelmux_cli-0.1.0/src/modelmux_cli/release.py +40 -0
  41. modelmux_cli-0.1.0/src/modelmux_cli/secrets_store.py +71 -0
  42. modelmux_cli-0.1.0/src/modelmux_cli/stack.py +217 -0
  43. modelmux_cli-0.1.0/src/modelmux_cli/terminal.py +44 -0
  44. modelmux_cli-0.1.0/src/modelmux_cli/updates.py +50 -0
  45. modelmux_cli-0.1.0/tests/__init__.py +0 -0
  46. modelmux_cli-0.1.0/tests/conftest.py +121 -0
  47. modelmux_cli-0.1.0/tests/fixtures/login/README.md +12 -0
  48. modelmux_cli-0.1.0/tests/fixtures/login/claude_browser.raw +3 -0
  49. modelmux_cli-0.1.0/tests/fixtures/login/codex_device.raw +14 -0
  50. modelmux_cli-0.1.0/tests/test_commands.py +254 -0
  51. modelmux_cli-0.1.0/tests/test_config_cmd.py +166 -0
  52. modelmux_cli-0.1.0/tests/test_console.py +118 -0
  53. modelmux_cli-0.1.0/tests/test_docker.py +185 -0
  54. modelmux_cli-0.1.0/tests/test_doctor_upgrade.py +263 -0
  55. modelmux_cli-0.1.0/tests/test_login.py +336 -0
  56. modelmux_cli-0.1.0/tests/test_main.py +101 -0
  57. modelmux_cli-0.1.0/tests/test_providers.py +146 -0
  58. modelmux_cli-0.1.0/tests/test_release.py +64 -0
  59. modelmux_cli-0.1.0/tests/test_source_scan.py +55 -0
  60. modelmux_cli-0.1.0/tests/test_stack.py +190 -0
  61. modelmux_cli-0.1.0/tests/test_storage.py +184 -0
  62. modelmux_cli-0.1.0/tests/test_terminal.py +142 -0
  63. modelmux_cli-0.1.0/uv.lock +952 -0
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ build/
7
+ dist/
8
+
9
+ # Tooling
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ coverage.xml
15
+ htmlcov/
16
+
17
+ # Local config and secrets
18
+ .env
19
+ .env.*
20
+ !.env.example
21
+ *.env
22
+ secrets/
23
+ *.pem
24
+ *.key
25
+
26
+ # Editors / OS
27
+ .vscode/
28
+ .idea/
29
+ .DS_Store
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.5
2
+ Name: modelmux-cli
3
+ Version: 0.1.0
4
+ Summary: Set up, log in to and run ModelMux: AI coding CLIs as an OpenAI-compatible API
5
+ Project-URL: Homepage, https://github.com/smit153/modelmux
6
+ Project-URL: Source, https://github.com/smit153/modelmux
7
+ Project-URL: Issues, https://github.com/smit153/modelmux/issues
8
+ License-Expression: Apache-2.0
9
+ Classifier: Environment :: Console
10
+ Classifier: Operating System :: MacOS
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # modelmux-cli
19
+
20
+ Set up, log in to and run [ModelMux](https://github.com/smit153/modelmux):
21
+ AI coding CLIs (Claude Code, Codex) as a secure, OpenAI-compatible API on your
22
+ own machine.
23
+
24
+ ```bash
25
+ pipx install modelmux-cli # or: uvx --from modelmux-cli modelmux
26
+ modelmux up # set up Docker containers, start logged-in providers
27
+ modelmux login claude # guided login (opens your browser), then a real test
28
+ modelmux config litellm # ready-to-paste config for LiteLLM, OpenAI SDK, LangChain...
29
+ ```
30
+
31
+ Other commands: `status`, `logs`, `doctor`, `upgrade`, `logout`, `key show`, `down`.
32
+
33
+ - Requires Docker with Compose v2 (Docker Desktop on macOS and Windows) and
34
+ Python 3.10+. No other dependencies.
35
+ - Each CLI release runs the server image built in the same release, pinned by
36
+ digest.
37
+ - Logins live in per-provider Docker volumes; the API key is generated
38
+ locally and stored with owner-only permissions.
39
+
40
+ Full documentation: <https://github.com/smit153/modelmux/blob/main/docs/CLI.md>
@@ -0,0 +1,23 @@
1
+ # modelmux-cli
2
+
3
+ Set up, log in to and run [ModelMux](https://github.com/smit153/modelmux):
4
+ AI coding CLIs (Claude Code, Codex) as a secure, OpenAI-compatible API on your
5
+ own machine.
6
+
7
+ ```bash
8
+ pipx install modelmux-cli # or: uvx --from modelmux-cli modelmux
9
+ modelmux up # set up Docker containers, start logged-in providers
10
+ modelmux login claude # guided login (opens your browser), then a real test
11
+ modelmux config litellm # ready-to-paste config for LiteLLM, OpenAI SDK, LangChain...
12
+ ```
13
+
14
+ Other commands: `status`, `logs`, `doctor`, `upgrade`, `logout`, `key show`, `down`.
15
+
16
+ - Requires Docker with Compose v2 (Docker Desktop on macOS and Windows) and
17
+ Python 3.10+. No other dependencies.
18
+ - Each CLI release runs the server image built in the same release, pinned by
19
+ digest.
20
+ - Logins live in per-provider Docker volumes; the API key is generated
21
+ locally and stored with owner-only permissions.
22
+
23
+ Full documentation: <https://github.com/smit153/modelmux/blob/main/docs/CLI.md>
@@ -0,0 +1,33 @@
1
+ """Bundle the repository's language-neutral /shared data into the package.
2
+
3
+ From the repository, the data is at ../../shared. Inside a source
4
+ distribution it was copied to ./shared, so wheels built from the sdist (as
5
+ pip and uv do) still get it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
14
+
15
+ PARTS = ("providers", "schema", "templates")
16
+ FILES = ("release.json",)
17
+
18
+
19
+ class SharedDataHook(BuildHookInterface): # type: ignore[type-arg]
20
+ PLUGIN_NAME = "custom"
21
+
22
+ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
23
+ root = Path(self.root)
24
+ for candidate in (root.parent.parent / "shared", root / "shared"):
25
+ if all((candidate / part).is_dir() for part in PARTS) and all(
26
+ (candidate / name).is_file() for name in FILES
27
+ ):
28
+ break
29
+ else:
30
+ raise RuntimeError("modelmux shared data (providers, schema) not found")
31
+ prefix = "modelmux_cli/_shared" if self.target_name == "wheel" else "shared"
32
+ for part in (*PARTS, *FILES):
33
+ build_data["force_include"][str(candidate / part)] = f"{prefix}/{part}"
@@ -0,0 +1,88 @@
1
+ [project]
2
+ name = "modelmux-cli"
3
+ version = "0.1.0"
4
+ description = "Set up, log in to and run ModelMux: AI coding CLIs as an OpenAI-compatible API"
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.10"
8
+ # Deliberately no runtime dependencies: this tool drives Docker on the user's
9
+ # machine, so it stays small and easy to audit.
10
+ dependencies = []
11
+ classifiers = [
12
+ "Environment :: Console",
13
+ "Operating System :: MacOS",
14
+ "Operating System :: Microsoft :: Windows",
15
+ "Operating System :: POSIX :: Linux",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Software Development",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/smit153/modelmux"
22
+ Source = "https://github.com/smit153/modelmux"
23
+ Issues = "https://github.com/smit153/modelmux/issues"
24
+
25
+ [project.scripts]
26
+ modelmux = "modelmux_cli.main:main"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "ruff>=0.6",
31
+ "mypy>=1.11",
32
+ "pytest>=8.3",
33
+ "pytest-cov>=5.0",
34
+ "jsonschema>=4.23",
35
+ "types-jsonschema>=4.23",
36
+ ]
37
+
38
+ [build-system]
39
+ requires = ["hatchling>=1.25"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/modelmux_cli"]
44
+
45
+ # The language-neutral data in /shared is bundled by hatch_build.py, both
46
+ # from the repository and when a wheel is built from the sdist.
47
+ [tool.hatch.build.hooks.custom]
48
+ path = "hatch_build.py"
49
+
50
+ # ---------------------------------------------------------------- ruff
51
+ [tool.ruff]
52
+ target-version = "py310"
53
+ line-length = 100
54
+ src = ["src", "tests"]
55
+
56
+ [tool.ruff.lint]
57
+ select = [
58
+ "E", "W", "F", "I", "B", "UP", "S", "N", "SIM", "RUF", "PL", "TID", "PT", "C4", "RET",
59
+ ]
60
+ ignore = ["PLR0913", "PLR2004"]
61
+
62
+ [tool.ruff.lint.pylint]
63
+ max-returns = 10
64
+
65
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
66
+ "subprocess".msg = "Run commands only via modelmux_cli.docker."
67
+ "os.system".msg = "No shells. Run commands only via modelmux_cli.docker."
68
+ "os.popen".msg = "No shells. Run commands only via modelmux_cli.docker."
69
+ "pty".msg = "Run commands only via modelmux_cli.docker."
70
+
71
+ [tool.ruff.lint.per-file-ignores]
72
+ "src/modelmux_cli/docker.py" = ["TID251", "S603", "S606"]
73
+ "tests/**" = ["S101", "S105", "S106", "S108", "TID251", "S603", "S607", "PLR0917"]
74
+
75
+ # ---------------------------------------------------------------- mypy
76
+ [tool.mypy]
77
+ python_version = "3.10"
78
+ strict = true
79
+ files = ["src"]
80
+
81
+ # ---------------------------------------------------------------- pytest
82
+ [tool.pytest.ini_options]
83
+ testpaths = ["tests"]
84
+ addopts = "-ra --strict-markers"
85
+
86
+ [tool.coverage.run]
87
+ source = ["modelmux_cli"]
88
+ branch = true
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "../schema/provider.schema.json",
3
+ "schema_version": 1,
4
+ "name": "claude",
5
+ "display_name": "Claude Code",
6
+ "driver": "claude",
7
+ "default_port": 8101,
8
+ "volume": "claude-home",
9
+ "home": "/home/modelmux/driver-home",
10
+ "example_model": "sonnet",
11
+ "login": {
12
+ "default_method": "browser",
13
+ "methods": {
14
+ "browser": {
15
+ "description": "Claude subscription: open a link, sign in, paste the code back",
16
+ "command": ["claude", "auth", "login"]
17
+ },
18
+ "console": {
19
+ "description": "Anthropic Console (API billing): open a link, sign in, paste the code back",
20
+ "command": ["claude", "auth", "login", "--console"]
21
+ }
22
+ },
23
+ "link_pattern": "https://\\S+",
24
+ "timeout_seconds": 600
25
+ },
26
+ "status": {
27
+ "command": ["claude", "auth", "status"],
28
+ "logged_in_exit_code": 0
29
+ },
30
+ "logout": {
31
+ "command": ["claude", "auth", "logout"]
32
+ },
33
+ "docs_url": "https://docs.anthropic.com/en/docs/claude-code"
34
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "../schema/provider.schema.json",
3
+ "schema_version": 1,
4
+ "name": "codex",
5
+ "display_name": "OpenAI Codex",
6
+ "driver": "codex",
7
+ "default_port": 8102,
8
+ "volume": "codex-home",
9
+ "home": "/home/modelmux/driver-home",
10
+ "example_model": "gpt-6.1-sol",
11
+ "login": {
12
+ "default_method": "device",
13
+ "methods": {
14
+ "device": {
15
+ "description": "ChatGPT account: open a link and enter the one-time code shown",
16
+ "command": ["codex", "login", "--device-auth"]
17
+ },
18
+ "api-key": {
19
+ "description": "OpenAI API key, read from your terminal without echo",
20
+ "command": ["codex", "login", "--with-api-key"],
21
+ "secret_stdin": "OpenAI API key"
22
+ }
23
+ },
24
+ "link_pattern": "https://\\S+",
25
+ "timeout_seconds": 900
26
+ },
27
+ "status": {
28
+ "command": ["codex", "login", "status"],
29
+ "logged_in_exit_code": 0
30
+ },
31
+ "logout": {
32
+ "command": ["codex", "logout"]
33
+ },
34
+ "docs_url": "https://developers.openai.com/codex/cli"
35
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "image": "ghcr.io/smit153/modelmux@sha256:1edfd02f98bf739c874fa0db043bdbd46d907ac41d2bc30fb7f5b8b311381b72"
4
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/smit153/modelmux/shared/schema/provider.schema.json",
4
+ "title": "ModelMux provider definition",
5
+ "description": "How the ModelMux CLI runs, logs in and checks one provider. Language-neutral: every CLI implementation reads these files.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema_version", "name", "display_name", "driver", "default_port", "volume", "home",
10
+ "example_model", "login", "status", "logout"
11
+ ],
12
+ "properties": {
13
+ "$schema": {"type": "string"},
14
+ "schema_version": {"const": 1},
15
+ "name": {"$ref": "#/$defs/identifier", "description": "Used on the command line: modelmux login <name>"},
16
+ "display_name": {"type": "string", "minLength": 1, "maxLength": 64},
17
+ "driver": {"$ref": "#/$defs/identifier", "description": "The server's MODELMUX_DRIVER value"},
18
+ "default_port": {"type": "integer", "minimum": 1024, "maximum": 65535},
19
+ "volume": {
20
+ "type": "string",
21
+ "pattern": "^[a-z0-9][a-z0-9_.-]{0,62}$",
22
+ "description": "Login volume name, unique per provider (prefixed with the compose project name)"
23
+ },
24
+ "home": {"type": "string", "pattern": "^/", "description": "HOME inside the container; the volume is mounted here"},
25
+ "example_model": {"type": "string", "minLength": 1, "maxLength": 64},
26
+ "login": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["default_method", "methods", "link_pattern", "timeout_seconds"],
30
+ "properties": {
31
+ "default_method": {"$ref": "#/$defs/identifier"},
32
+ "methods": {
33
+ "type": "object",
34
+ "minProperties": 1,
35
+ "propertyNames": {"$ref": "#/$defs/identifier"},
36
+ "additionalProperties": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["description", "command"],
40
+ "properties": {
41
+ "description": {"type": "string", "minLength": 1},
42
+ "command": {"$ref": "#/$defs/command"},
43
+ "secret_stdin": {
44
+ "type": "string",
45
+ "minLength": 1,
46
+ "description": "If set, prompt for this secret without echo and pass it on stdin"
47
+ }
48
+ }
49
+ }
50
+ },
51
+ "link_pattern": {"type": "string", "minLength": 1, "description": "Regex that finds the login link in the CLI's output"},
52
+ "timeout_seconds": {"type": "integer", "minimum": 30, "maximum": 3600}
53
+ }
54
+ },
55
+ "status": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["command", "logged_in_exit_code"],
59
+ "properties": {
60
+ "command": {"$ref": "#/$defs/command"},
61
+ "logged_in_exit_code": {"type": "integer"}
62
+ }
63
+ },
64
+ "logout": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "required": ["command"],
68
+ "properties": {"command": {"$ref": "#/$defs/command"}}
69
+ },
70
+ "docs_url": {"type": "string", "pattern": "^https://"}
71
+ },
72
+ "$defs": {
73
+ "identifier": {"type": "string", "pattern": "^[a-z][a-z0-9-]{0,31}$"},
74
+ "command": {
75
+ "type": "array",
76
+ "minItems": 1,
77
+ "description": "argv run inside a helper container; the first item is the entrypoint. Never a shell string.",
78
+ "items": {"type": "string", "pattern": "^[A-Za-z0-9._=/-]+$"}
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "read_only": true,
3
+ "tmpfs": ["/tmp:rw,noexec,nosuid,nodev,size=512m"],
4
+ "cap_drop": ["ALL"],
5
+ "security_opt": ["no-new-privileges:true"],
6
+ "user": "10001:10001",
7
+ "pids_limit": 256,
8
+ "mem_limit": "2g",
9
+ "cpus": 2,
10
+ "restart": "on-failure:3",
11
+ "labels": {"io.modelmux.managed": "true"}
12
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "curl request",
3
+ "per": "provider",
4
+ "key_style": "shell",
5
+ "header": "",
6
+ "entry": "# {{display_name}}\ncurl {{base_url}}/chat/completions \\\n -H \"Authorization: Bearer {{key}}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\": \"{{model}}\", \"messages\": [{\"role\": \"user\", \"content\": \"Hello!\"}]}'\n\n",
7
+ "footer": ""
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "Environment variables (.env)",
3
+ "per": "provider",
4
+ "key_style": "env",
5
+ "header": "MODELMUX_API_KEY={{key}}\n",
6
+ "entry": "MODELMUX_{{PROVIDER}}_BASE_URL={{base_url}}\nMODELMUX_{{PROVIDER}}_MODEL={{model}}\n",
7
+ "footer": ""
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "LangChain ChatOpenAI",
3
+ "per": "provider",
4
+ "key_style": "python",
5
+ "header": "import os\n\nfrom langchain_openai import ChatOpenAI\n",
6
+ "entry": "\n# {{display_name}}\n{{provider_var}} = ChatOpenAI(\n model=\"{{model}}\",\n base_url=\"{{base_url}}\",\n api_key={{key}},\n use_responses_api=False, # ModelMux implements Chat Completions\n)\nprint({{provider_var}}.invoke(\"Hello!\").content)\n",
7
+ "footer": ""
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "LiteLLM proxy config.yaml (model_list)",
3
+ "per": "model",
4
+ "key_style": "litellm",
5
+ "header": "model_list:\n",
6
+ "entry": " - model_name: {{provider}}-{{model}}\n litellm_params:\n model: openai/{{model}}\n api_base: {{base_url}}\n api_key: {{key}}\n",
7
+ "footer": ""
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "OpenAI Python SDK",
3
+ "per": "provider",
4
+ "key_style": "python",
5
+ "header": "import os\n\nfrom openai import OpenAI\n",
6
+ "entry": "\n# {{display_name}}\n{{provider_var}} = OpenAI(base_url=\"{{base_url}}\", api_key={{key}})\nreply = {{provider_var}}.chat.completions.create(\n model=\"{{model}}\",\n messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n)\nprint(reply.choices[0].message.content)\n",
7
+ "footer": ""
8
+ }
@@ -0,0 +1,8 @@
1
+ """modelmux-cli: set up, log in to and run ModelMux."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("modelmux-cli")
7
+ except PackageNotFoundError: # pragma: no cover - running from a source tree
8
+ __version__ = "0.0.0"
@@ -0,0 +1,7 @@
1
+ """``python -m modelmux_cli``."""
2
+
3
+ import sys
4
+
5
+ from modelmux_cli.main import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,87 @@
1
+ """Render client config snippets from ``shared/templates/config/<target>.json``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from modelmux_cli.errors import CliError
11
+ from modelmux_cli.providers import shared_dir
12
+
13
+ KEY_ENV = "MODELMUX_API_KEY"
14
+ TARGETS = ("litellm", "openai-python", "langchain", "curl", "env")
15
+
16
+ # How each target refers to the key: (by reference, when revealed).
17
+ _KEY_STYLES: dict[str, tuple[str, Any]] = {
18
+ "litellm": (f"os.environ/{KEY_ENV}", lambda key: key),
19
+ "python": (f'os.environ["{KEY_ENV}"]', json.dumps),
20
+ "shell": (f"${KEY_ENV}", lambda key: key),
21
+ "env": ("", lambda key: key),
22
+ }
23
+ _PLACEHOLDER = re.compile(r"\{\{(\w+)\}\}")
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Template:
28
+ name: str
29
+ description: str
30
+ per: str
31
+ key_style: str
32
+ header: str
33
+ entry: str
34
+ footer: str
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Target:
39
+ provider: str
40
+ display_name: str
41
+ base_url: str
42
+ models: tuple[str, ...]
43
+
44
+
45
+ def load_template(name: str) -> Template:
46
+ path = shared_dir() / "templates" / "config" / f"{name}.json"
47
+ try:
48
+ data = json.loads(path.read_text(encoding="utf-8"))
49
+ template = Template(name=name, **{k: data[k] for k in (
50
+ "description", "per", "key_style", "header", "entry", "footer")}) # fmt: skip
51
+ except (OSError, ValueError, KeyError, TypeError):
52
+ raise CliError(f"The {name!r} config template is missing or invalid.",
53
+ hint="Please report this bug.") from None # fmt: skip
54
+ if template.per not in {"provider", "model"} or template.key_style not in _KEY_STYLES:
55
+ raise CliError(f"The {name!r} config template is invalid.", hint="Please report this bug.")
56
+ return template
57
+
58
+
59
+ def _fill(text: str, values: dict[str, str]) -> str:
60
+ def replace(match: re.Match[str]) -> str:
61
+ if match.group(1) not in values:
62
+ raise CliError(f"Unknown placeholder {match.group(0)} in a config template.",
63
+ hint="Please report this bug.") # fmt: skip
64
+ return values[match.group(1)]
65
+
66
+ return _PLACEHOLDER.sub(replace, text)
67
+
68
+
69
+ def render(template: Template, targets: list[Target], api_key: str | None) -> str:
70
+ """``api_key`` None means: refer to $MODELMUX_API_KEY instead of the value."""
71
+ reference, literal = _KEY_STYLES[template.key_style]
72
+ key = reference if api_key is None else literal(api_key)
73
+ parts = [_fill(template.header, {"key": key})]
74
+ for target in targets:
75
+ models = target.models if template.per == "model" else target.models[:1]
76
+ for model in models:
77
+ parts.append(_fill(template.entry, {
78
+ "provider": target.provider,
79
+ "PROVIDER": target.provider.upper().replace("-", "_"),
80
+ "provider_var": target.provider.replace("-", "_"),
81
+ "display_name": target.display_name,
82
+ "model": model,
83
+ "base_url": target.base_url,
84
+ "key": key,
85
+ })) # fmt: skip
86
+ parts.append(_fill(template.footer, {"key": key}))
87
+ return "".join(parts).rstrip("\n") + "\n"
@@ -0,0 +1 @@
1
+ """One module per command. Each exposes ``run(args, console) -> exit code``."""
@@ -0,0 +1,75 @@
1
+ """``modelmux down``, ``logs`` and ``status``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from modelmux_cli import health
8
+ from modelmux_cli.console import Console
9
+ from modelmux_cli.context import make_context
10
+ from modelmux_cli.errors import CliError
11
+ from modelmux_cli.stack import resolve_image, service_name
12
+
13
+ NOT_SET_UP = "ModelMux is not set up on this machine yet."
14
+
15
+
16
+ def down(args: argparse.Namespace, console: Console) -> int:
17
+ ctx = make_context(console)
18
+ if not ctx.stack.compose_file.exists():
19
+ console.success("ModelMux is not running.")
20
+ return 0
21
+ ctx.stack.compose("down", timeout=120)
22
+ console.success("Stopped ModelMux. Your logins are kept.")
23
+ return 0
24
+
25
+
26
+ def logs(args: argparse.Namespace, console: Console) -> int:
27
+ ctx = make_context(console)
28
+ stack = ctx.stack
29
+ if not stack.compose_file.exists():
30
+ raise CliError(NOT_SET_UP, hint="Start it with: modelmux up")
31
+ services = [service_name(ctx.providers[args.provider])] if args.provider else []
32
+ follow = ["--follow"] if args.follow else []
33
+ return ctx.docker.passthrough(
34
+ "compose", "-p", "modelmux", "-f", str(stack.compose_file),
35
+ "logs", "--no-log-prefix" if args.provider else "--timestamps",
36
+ "--tail", str(max(args.tail, 0)), *follow, *services,
37
+ ) # fmt: skip
38
+
39
+
40
+ def status(args: argparse.Namespace, console: Console) -> int:
41
+ ctx = make_context(console)
42
+ ctx.docker.check_available()
43
+ stack = ctx.stack
44
+ states = stack.states()
45
+ try:
46
+ image: str | None = resolve_image(ctx.config)
47
+ except CliError:
48
+ image = None
49
+
50
+ rows = [("PROVIDER", "CONTAINER", "LOGIN", "HEALTH", "URL")]
51
+ for name, provider in sorted(ctx.providers.items()):
52
+ state = states.get(service_name(provider))
53
+ port = ctx.config.port(name)
54
+ container = state.state if state else "not started"
55
+ login = "?" if image is None else ("yes" if stack.logged_in(provider, image) else "no")
56
+ if state and state.state == "running":
57
+ healthy = "ready" if health.ready(port) else "starting"
58
+ else:
59
+ healthy = "-"
60
+ url = f"{health.base_url(port)}/v1" if healthy == "ready" else "-"
61
+ rows.append((name, container, login, healthy, url))
62
+
63
+ widths = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))]
64
+ for row in rows:
65
+ console.print(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)).rstrip())
66
+
67
+ if image is None:
68
+ console.step("No server image chosen yet: run 'modelmux up' first.")
69
+ for name, provider in sorted(ctx.providers.items()):
70
+ state = states.get(service_name(provider))
71
+ if state and state.state in {"exited", "dead"}:
72
+ console.step(
73
+ f"{provider.display_name} has stopped. If its login expired: modelmux login {name}"
74
+ )
75
+ return 0