From b34d49b9f590b5bb80c20ebda0bbb28b19591d54 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 8 Apr 2026 20:50:17 -0400 Subject: [PATCH 1/5] feat: add support for specifying model alias used in requests and responses This commit adds an optional `--alias` argument to both `ramalama serve` and `ramalama run`. - For the llama.cpp runtime, this sets the --alias argument - For the vllm runtime, this sets the --served-model-name argument - For the mlx runtime, this argument is ignored This argument is useful if you are testing multiple different models, since you can use the same configuration for them provided they have the same alias (for example, the "AI Chat" feature in Jetbrains IDEs). Example Usage: ```bash ramalama serve --alias local gpt-oss ``` Once the server is up, `http://localhost:8080/v1/models` will use `local` as the model's name: ```json { "models": [ { "name": "local", "model": "local", ... } ], "data": [ { "id": "local", ... } ], ... } ``` Fixes https://github.com/containers/ramalama/issues/2591. Signed-off-by: Christopher Chianelli --- ramalama/cli.py | 6 ++++ ramalama/engine.py | 2 ++ .../runtimes/inference/llama_cpp_commands.py | 4 ++- ramalama/plugins/runtimes/inference/vllm.py | 5 ++- ramalama/transports/base.py | 2 +- test/e2e/test_run.py | 16 +++++++++ test/unit/test_engine.py | 11 ++++++ test/unit/test_inference_engine_plugins.py | 35 +++++++++++++++++++ 8 files changed, 78 insertions(+), 3 deletions(-) diff --git a/ramalama/cli.py b/ramalama/cli.py index 40e279aa6..80edc68eb 100644 --- a/ramalama/cli.py +++ b/ramalama/cli.py @@ -887,6 +887,12 @@ def runtime_options(parser, command): help="name of container in which the Model will be run", completer=suppressCompleter, ) + parser.add_argument( + "--alias", + dest="alias", + help="model name alias (referenced in the requests and responses of the API)", + completer=suppressCompleter, + ) add_network_argument(parser, dflt=None) parser.add_argument( "--oci-runtime", diff --git a/ramalama/engine.py b/ramalama/engine.py index b9b959255..ff5fc3c33 100644 --- a/ramalama/engine.py +++ b/ramalama/engine.py @@ -479,6 +479,8 @@ def is_healthy(args, timeout: int = 3, model_name: Optional[str] = None): from ramalama.plugins.loader import get_runtime conn = None + if getattr(args, "alias", None): + model_name = args.alias try: conn = HTTPConnection("127.0.0.1", args.port, timeout=timeout) if getattr(args, "debug", False): diff --git a/ramalama/plugins/runtimes/inference/llama_cpp_commands.py b/ramalama/plugins/runtimes/inference/llama_cpp_commands.py index c65254b8a..1a9bd374c 100644 --- a/ramalama/plugins/runtimes/inference/llama_cpp_commands.py +++ b/ramalama/plugins/runtimes/inference/llama_cpp_commands.py @@ -83,7 +83,9 @@ def _cmd_run(self, args: argparse.Namespace) -> list[str]: if not getattr(args, 'thinking', None): cmd += ["--reasoning-budget", "0"] - if model is not None: + if getattr(args, 'alias', None): + cmd += ["--alias", args.alias] + elif model is not None: cmd += ["--alias", model.model_alias] ctx_size = getattr(args, 'ctx_size', None) diff --git a/ramalama/plugins/runtimes/inference/vllm.py b/ramalama/plugins/runtimes/inference/vllm.py index 20e0e2b2f..fede2d206 100644 --- a/ramalama/plugins/runtimes/inference/vllm.py +++ b/ramalama/plugins/runtimes/inference/vllm.py @@ -42,7 +42,10 @@ def _cmd_run(self, args: argparse.Namespace) -> list[str]: if model is not None: model_path = model._get_entry_model_path(is_container, should_generate, dry_run) cmd += ["--model", model_path] - cmd += ["--served-model-name", model.model_alias] + if getattr(args, 'alias', None): + cmd += ["--served-model-name", args.alias] + else: + cmd += ["--served-model-name", model.model_alias] ctx_size = getattr(args, 'ctx_size', None) if ctx_size: diff --git a/ramalama/transports/base.py b/ramalama/transports/base.py index 82fcaff56..787ac102f 100644 --- a/ramalama/transports/base.py +++ b/ramalama/transports/base.py @@ -505,7 +505,7 @@ def _connect_and_chat(self, args, server_process): # Model name in the chat request must match RamalamaModelContext.alias() chat_args = copy.deepcopy(args) - chat_args.model = f"{self.model_organization}/{self.model_name}" + chat_args.model = getattr(args, 'alias', f"{self.model_organization}/{self.model_name}") if args.container: return self._handle_container_chat(chat_args, server_process) diff --git a/test/e2e/test_run.py b/test/e2e/test_run.py index 387eb0cd0..6d0dba10e 100644 --- a/test/e2e/test_run.py +++ b/test/e2e/test_run.py @@ -289,6 +289,22 @@ def test_run_model_with_prompt(shared_ctx_with_models, test_model): ctx.check_call(run_cmd) +@pytest.mark.e2e +@pytest.mark.slow +def test_run_model_with_prompt_and_alias(shared_ctx_with_models, test_model): + import platform + + ctx = shared_ctx_with_models + + run_cmd = ["ramalama", "run", "--temp", "0", "--alias", "my_alias"] + if platform.system() in ["Darwin", "Windows"]: + # FIXME: continues rambling on Windows and macOS without --max-token + run_cmd.extend(["--max-tokens", "100"]) + + run_cmd.extend([test_model, "Who is the primary writer of the declaration of independence?"]) + ctx.check_call(run_cmd) + + _file_uri_id_suffix = 'C:/dir/file' if platform.system() == "Windows" else '/absolute_dir/file' _file_uri_id_relative = "relative_dir/file" diff --git a/test/unit/test_engine.py b/test/unit/test_engine.py index c6db34e4e..86d380181 100644 --- a/test/unit/test_engine.py +++ b/test/unit/test_engine.py @@ -165,6 +165,17 @@ def test_is_healthy_conn(mock_conn): mock_conn.assert_called_once_with("127.0.0.1", args.port, timeout=3) +@patch("ramalama.engine.HTTPConnection") +@patch("ramalama.plugins.loader.get_runtime") +def test_is_healthy_conn_with_alias(mock_runtime, mock_conn): + args = Namespace(MODEL="themodel", name="thecontainer", port=8080, debug=False, alias='my_alias') + mock_service_ready_check = Mock() + mock_runtime.return_value = Namespace(service_ready_check=mock_service_ready_check) + ramalama.engine.is_healthy(args, model_name="themodel") + mock_conn.assert_called_once_with("127.0.0.1", args.port, timeout=3) + mock_service_ready_check.assert_called_once_with(mock_conn.return_value, args, 'my_alias') + + @pytest.mark.parametrize( "health_status, models_status, models_body, models_msg", [ diff --git a/test/unit/test_inference_engine_plugins.py b/test/unit/test_inference_engine_plugins.py index 129edf83c..792561593 100644 --- a/test/unit/test_inference_engine_plugins.py +++ b/test/unit/test_inference_engine_plugins.py @@ -34,6 +34,7 @@ def make_ns( max_tokens=0, port="8080", host="::", + alias=None, logfile=None, debug=False, webui="on", @@ -56,6 +57,7 @@ def make_ns( max_tokens=max_tokens, port=port, host=host, + alias=alias, logfile=logfile, debug=debug, webui=webui, @@ -156,6 +158,7 @@ def test_serve_basic(self, mock_colorize, mock_new, container_image_is_ggml): assert cmd[cmd.index("--model") + 1] == "/mnt/models/model.file" assert "--no-warmup" in cmd assert "--alias" in cmd + assert cmd[cmd.index("--alias") + 1] == 'mymodel' assert "-ngl" in cmd @patch("ramalama.plugins.runtimes.inference.llama_cpp_commands.should_colorize", return_value=False) @@ -297,6 +300,26 @@ def test_serve_seed(self, mock_colorize): assert "--seed" in cmd assert cmd[cmd.index("--seed") + 1] == "42" + @patch("ramalama.plugins.runtimes.inference.llama_cpp_commands.should_colorize", return_value=False) + def test_serve_alias(self, mock_colorize): + ns = make_ns(alias='my_alias') + cmd = self.plugin.handle_subcommand("serve", ns) + + assert "--alias" in cmd + assert cmd[cmd.index("--alias") + 1] == "my_alias" + + @patch("ramalama.plugins.runtimes.inference.llama_cpp_commands.New") + @patch("ramalama.plugins.runtimes.inference.llama_cpp_commands.should_colorize", return_value=False) + def test_serve_alias_override_model_alias(self, mock_colorize, mock_new): + mock_model = make_transport_model(chat_template_path="/mnt/models/chat_template.file") + mock_new.return_value = mock_model + + ns = make_ns(alias="my_alias", MODEL="ollama://mymodel") + cmd = self.plugin.handle_subcommand("serve", ns) + + assert "--alias" in cmd + assert cmd[cmd.index("--alias") + 1] == "my_alias" + @patch("ramalama.plugins.runtimes.inference.llama_cpp_commands.should_colorize", return_value=False) def test_serve_debug(self, mock_colorize): ns = make_ns(debug=True) @@ -514,6 +537,7 @@ def test_serve_in_container(self, mock_new): assert isinstance(cmd[0], ContainerEntryPoint) assert "--model" in cmd assert "--served-model-name" in cmd + assert cmd[cmd.index("--served-model-name") + 1] == "mymodel" assert "--port" in cmd def test_serve_nocontainer(self): @@ -544,6 +568,17 @@ def test_serve_seed(self): assert "--seed" in cmd assert cmd[cmd.index("--seed") + 1] == "123" + @patch("ramalama.plugins.runtimes.inference.vllm.New") + def test_serve_alias(self, mock_new): + mock_model = make_transport_model(chat_template_path="/mnt/models/chat_template.file") + mock_new.return_value = mock_model + + ns = make_ns(alias="my_alias", MODEL="ollama://mymodel") + cmd = self.plugin.handle_subcommand("serve", ns) + + assert "--served-model-name" in cmd + assert cmd[cmd.index("--served-model-name") + 1] == "my_alias" + def test_serve_seed_not_added_when_none(self): ns = make_ns(seed=None) cmd = self.plugin.handle_subcommand("serve", ns) From 567316f03db6e084e1420a51b87ebeb64b7f1e1e Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 9 Apr 2026 21:14:33 -0400 Subject: [PATCH 2/5] chore: move --alias handling to applicable inference engines Also extended the comment in `cli.py` so future people extending `run` and `serve` know where to find the applicable argument parsers. Signed-off-by: Christopher Chianelli --- ramalama/cli.py | 8 ++--- ramalama/engine.py | 2 -- .../plugins/runtimes/inference/llama_cpp.py | 9 +++++- ramalama/plugins/runtimes/inference/vllm.py | 11 +++++++ ramalama/transports/base.py | 2 +- test/unit/test_engine.py | 31 ++++++++++++------- 6 files changed, 42 insertions(+), 21 deletions(-) diff --git a/ramalama/cli.py b/ramalama/cli.py index 80edc68eb..84eb5e7be 100644 --- a/ramalama/cli.py +++ b/ramalama/cli.py @@ -337,6 +337,8 @@ def configure_subcommands(parser): # output only shows subcommands the active runtime actually supports. # get_config().runtime is already set by Phase 1 of parse_args_from_cmd # before configure_subcommands() is called in Phase 2. + # These include the subcommands serve and run, which are handled + # in BaseInferenceRuntime and its subclasses runtime = ActiveConfig().runtime get_runtime(runtime).register_subcommands(subparsers) chat_parser(subparsers) @@ -887,12 +889,6 @@ def runtime_options(parser, command): help="name of container in which the Model will be run", completer=suppressCompleter, ) - parser.add_argument( - "--alias", - dest="alias", - help="model name alias (referenced in the requests and responses of the API)", - completer=suppressCompleter, - ) add_network_argument(parser, dflt=None) parser.add_argument( "--oci-runtime", diff --git a/ramalama/engine.py b/ramalama/engine.py index ff5fc3c33..b9b959255 100644 --- a/ramalama/engine.py +++ b/ramalama/engine.py @@ -479,8 +479,6 @@ def is_healthy(args, timeout: int = 3, model_name: Optional[str] = None): from ramalama.plugins.loader import get_runtime conn = None - if getattr(args, "alias", None): - model_name = args.alias try: conn = HTTPConnection("127.0.0.1", args.port, timeout=timeout) if getattr(args, "debug", False): diff --git a/ramalama/plugins/runtimes/inference/llama_cpp.py b/ramalama/plugins/runtimes/inference/llama_cpp.py index d9cf4acc4..bbe0efd93 100644 --- a/ramalama/plugins/runtimes/inference/llama_cpp.py +++ b/ramalama/plugins/runtimes/inference/llama_cpp.py @@ -224,7 +224,8 @@ def service_ready_check(self, conn: HTTPConnection, args: Any, model_name: Optio if not model_name: model_name = New(args.MODEL, args).model_alias - if model_name not in model_names: + model_id = getattr(args, 'alias', None) or model_name + if model_id not in model_names: logger.debug( f'{self.name} {container_name} /models does not include "{model_name}" in the model list: {model_names}' ) @@ -352,6 +353,12 @@ def _add_inference_args(self, parser: "argparse.ArgumentParser", command: str) - default="on", help="enable or disable the web UI (default: on)", ) + parser.add_argument( + "--alias", + dest="alias", + help="model name alias (referenced in the requests and responses of the API)", + completer=suppressCompleter, + ) @staticmethod def _set_openvino_env(args: argparse.Namespace) -> None: diff --git a/ramalama/plugins/runtimes/inference/vllm.py b/ramalama/plugins/runtimes/inference/vllm.py index fede2d206..2636a4a09 100644 --- a/ramalama/plugins/runtimes/inference/vllm.py +++ b/ramalama/plugins/runtimes/inference/vllm.py @@ -94,6 +94,17 @@ def _register_serve_subcommand(self, subparsers: "argparse._SubParsersAction") - self._add_max_model_len_arg(parser) return parser + def _add_inference_args(self, parser: "argparse.ArgumentParser", command: str) -> None: + """Add llama.cpp-specific inference args to an already-created parser.""" + super()._add_inference_args(parser, command) + if command == "serve": + parser.add_argument( + "--alias", + dest="alias", + help="model name alias (referenced in the requests and responses of the API)", + completer=suppressCompleter, + ) + def get_container_image(self, config: Any, detected_gpu_type: str) -> Optional[str]: if detected_gpu_type: image = config.images.get(f"VLLM_{detected_gpu_type}") or _VLLM_IMAGES.get(detected_gpu_type) diff --git a/ramalama/transports/base.py b/ramalama/transports/base.py index 787ac102f..d75a5fe1a 100644 --- a/ramalama/transports/base.py +++ b/ramalama/transports/base.py @@ -505,7 +505,7 @@ def _connect_and_chat(self, args, server_process): # Model name in the chat request must match RamalamaModelContext.alias() chat_args = copy.deepcopy(args) - chat_args.model = getattr(args, 'alias', f"{self.model_organization}/{self.model_name}") + chat_args.model = getattr(args, 'alias', None) or self.model_alias if args.container: return self._handle_container_chat(chat_args, server_process) diff --git a/test/unit/test_engine.py b/test/unit/test_engine.py index 86d380181..4f2522501 100644 --- a/test/unit/test_engine.py +++ b/test/unit/test_engine.py @@ -165,17 +165,6 @@ def test_is_healthy_conn(mock_conn): mock_conn.assert_called_once_with("127.0.0.1", args.port, timeout=3) -@patch("ramalama.engine.HTTPConnection") -@patch("ramalama.plugins.loader.get_runtime") -def test_is_healthy_conn_with_alias(mock_runtime, mock_conn): - args = Namespace(MODEL="themodel", name="thecontainer", port=8080, debug=False, alias='my_alias') - mock_service_ready_check = Mock() - mock_runtime.return_value = Namespace(service_ready_check=mock_service_ready_check) - ramalama.engine.is_healthy(args, model_name="themodel") - mock_conn.assert_called_once_with("127.0.0.1", args.port, timeout=3) - mock_service_ready_check.assert_called_once_with(mock_conn.return_value, args, 'my_alias') - - @pytest.mark.parametrize( "health_status, models_status, models_body, models_msg", [ @@ -245,6 +234,26 @@ def test_is_healthy_success(mock_conn, mock_debug, health_status): assert mock_debug.call_args.args[0] == "llama.cpp server is ready" +@pytest.mark.parametrize( + "health_status", + [ + pytest.param(200, id="health api ok"), + pytest.param(404, id="no health api"), + ], +) +@patch("ramalama.engine.logger.debug") +@patch("ramalama.engine.HTTPConnection") +def test_is_healthy_success_with_alias(mock_conn, mock_debug, health_status): + mock_health_resp = Mock(status=health_status) + mock_models_resp = Mock(status=200) + mock_models_resp.read.return_value = '{"models": [{"name": "alias"}]}' + mock_conn.return_value.getresponse.side_effect = [mock_health_resp, mock_models_resp] + args = Namespace(MODEL="themodel", name="thecontainer", port=8080, debug=False, alias='alias') + assert ramalama.engine.is_healthy(args, model_name="themodel") + assert mock_conn.return_value.getresponse.call_count == 2 + assert mock_debug.call_args.args[0] == "llama.cpp server is ready" + + @pytest.mark.parametrize( "status, ok", [ From 50853fb4dbf0c33e1c6676486113de404f093ef0 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 9 Apr 2026 21:36:03 -0400 Subject: [PATCH 3/5] chore: fix vLLM _add_inference_args docstring Signed-off-by: Christopher Chianelli --- ramalama/plugins/runtimes/inference/vllm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ramalama/plugins/runtimes/inference/vllm.py b/ramalama/plugins/runtimes/inference/vllm.py index 2636a4a09..b192df307 100644 --- a/ramalama/plugins/runtimes/inference/vllm.py +++ b/ramalama/plugins/runtimes/inference/vllm.py @@ -95,7 +95,7 @@ def _register_serve_subcommand(self, subparsers: "argparse._SubParsersAction") - return parser def _add_inference_args(self, parser: "argparse.ArgumentParser", command: str) -> None: - """Add llama.cpp-specific inference args to an already-created parser.""" + """Add vLLM-specific inference args to an already-created parser.""" super()._add_inference_args(parser, command) if command == "serve": parser.add_argument( From d20e513770fc5c55a7e421312c3ac032cd908756 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 13 Apr 2026 18:28:03 -0400 Subject: [PATCH 4/5] chore: Move test for alias from run to serve Additionally, the test now verifies the models endpoint returns the alias instead of the default name. Signed-off-by: Christopher Chianelli --- test/e2e/test_run.py | 16 ---------------- test/e2e/test_serve.py | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/test/e2e/test_run.py b/test/e2e/test_run.py index 6d0dba10e..387eb0cd0 100644 --- a/test/e2e/test_run.py +++ b/test/e2e/test_run.py @@ -289,22 +289,6 @@ def test_run_model_with_prompt(shared_ctx_with_models, test_model): ctx.check_call(run_cmd) -@pytest.mark.e2e -@pytest.mark.slow -def test_run_model_with_prompt_and_alias(shared_ctx_with_models, test_model): - import platform - - ctx = shared_ctx_with_models - - run_cmd = ["ramalama", "run", "--temp", "0", "--alias", "my_alias"] - if platform.system() in ["Darwin", "Windows"]: - # FIXME: continues rambling on Windows and macOS without --max-token - run_cmd.extend(["--max-tokens", "100"]) - - run_cmd.extend([test_model, "Who is the primary writer of the declaration of independence?"]) - ctx.check_call(run_cmd) - - _file_uri_id_suffix = 'C:/dir/file' if platform.system() == "Windows" else '/absolute_dir/file' _file_uri_id_relative = "relative_dir/file" diff --git a/test/e2e/test_serve.py b/test/e2e/test_serve.py index 4f0c407eb..bd3cda6b2 100644 --- a/test/e2e/test_serve.py +++ b/test/e2e/test_serve.py @@ -12,6 +12,7 @@ from subprocess import STDOUT, CalledProcessError import pytest +import requests import yaml from test.conftest import ( @@ -336,6 +337,27 @@ def test_serve_and_stop(shared_ctx, test_model): assert not re.search(f".*({container1_id}|{container2_id})", ps_result) +@pytest.mark.e2e +@pytest.mark.slow +@skip_if_no_container +def test_serve_model_with_alias(shared_ctx, test_model): + container_id = f"serve_with_alias_{''.join(random.choices(string.ascii_letters + string.digits, k=5))}" + alias = "my_alias" + ctx = shared_ctx + serve_cmd = ["ramalama", "serve", "--name", container_id, "--alias", alias, "--detach", test_model] + ctx.check_call(serve_cmd) + try: + ps_list = ctx.check_output(["ramalama", "ps", "--format", "{{.Names}} {{.Ports}}"]) + port = re.search(rf"{container_id}.*->(?P\d+)", ps_list)["port"] + # FIXME: race-condition, chat can fail to connect if llama.cpp isn't ready, just sleep a little for now + time.sleep(10) + models = requests.get(f"http://127.0.0.1:{port}/v1/models").json() + assert models["models"][0]["name"] == alias + assert models["models"][0]["model"] == alias + finally: + ctx.check_call(["ramalama", "stop", container_id]) + + @pytest.mark.e2e @pytest.mark.slow @skip_if_no_container From 9537d05f1e2ca2904121b6d3853931d2ae198536 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Sun, 19 Apr 2026 14:21:59 -0400 Subject: [PATCH 5/5] docs: include alias in docs Signed-off-by: Christopher Chianelli --- docs/options/alias.md | 6 ++++++ docs/ramalama-sandbox-goose.1.md.in | 2 ++ docs/ramalama-sandbox-opencode.1.md.in | 2 ++ docs/ramalama-serve.1.md.in | 2 ++ 4 files changed, 12 insertions(+) create mode 100644 docs/options/alias.md diff --git a/docs/options/alias.md b/docs/options/alias.md new file mode 100644 index 000000000..ae49655a2 --- /dev/null +++ b/docs/options/alias.md @@ -0,0 +1,6 @@ +####> This option file is used in: +####> ramalama sandbox goose, ramalama sandbox opencode, ramalama serve +####> If this file is edited, make sure the changes +####> are applicable to all of those. +#### **--alias** +Model name alias (referenced in the requests and responses of the API). diff --git a/docs/ramalama-sandbox-goose.1.md.in b/docs/ramalama-sandbox-goose.1.md.in index 9f4bd1b93..37850cbdd 100644 --- a/docs/ramalama-sandbox-goose.1.md.in +++ b/docs/ramalama-sandbox-goose.1.md.in @@ -21,6 +21,8 @@ exits, the model server container is automatically stopped and removed. ## OPTIONS +@@option alias + @@option authfile @@option backend diff --git a/docs/ramalama-sandbox-opencode.1.md.in b/docs/ramalama-sandbox-opencode.1.md.in index f06b3d42c..10e8639b0 100644 --- a/docs/ramalama-sandbox-opencode.1.md.in +++ b/docs/ramalama-sandbox-opencode.1.md.in @@ -21,6 +21,8 @@ exits, the model server container is automatically stopped and removed. ## OPTIONS +@@option alias + @@option authfile @@option backend diff --git a/docs/ramalama-serve.1.md.in b/docs/ramalama-serve.1.md.in index 4c612d68b..14630423f 100644 --- a/docs/ramalama-serve.1.md.in +++ b/docs/ramalama-serve.1.md.in @@ -34,6 +34,8 @@ Useful, for instance, to add environment variables to the generated unit file, o Section, key and value are required and must be separated by colons. +@@option alias + @@option api @@option authfile