Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/options/alias.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 2 additions & 0 deletions docs/ramalama-sandbox-goose.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ exits, the model server container is automatically stopped and removed.

## OPTIONS

@@option alias

@@option authfile

@@option backend
Expand Down
2 changes: 2 additions & 0 deletions docs/ramalama-sandbox-opencode.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ exits, the model server container is automatically stopped and removed.

## OPTIONS

@@option alias

@@option authfile

@@option backend
Expand Down
2 changes: 2 additions & 0 deletions docs/ramalama-serve.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions ramalama/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't seem relevant to this PR

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It a note for when a future developer wants to add an arg specific to serve and run; it would of helped me realize where to put the parser code!

# in BaseInferenceRuntime and its subclasses
runtime = ActiveConfig().runtime
get_runtime(runtime).register_subcommands(subparsers)
chat_parser(subparsers)
Expand Down
9 changes: 8 additions & 1 deletion ramalama/plugins/runtimes/inference/llama_cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
)
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion ramalama/plugins/runtimes/inference/llama_cpp_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion ramalama/plugins/runtimes/inference/vllm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -91,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 vLLM-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,
)
Comment thread
mikebonnet marked this conversation as resolved.

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)
Expand Down
2 changes: 1 addition & 1 deletion ramalama/transports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', None) or self.model_alias

@coderabbitai coderabbitai Bot Apr 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize multi-alias input before setting chat model ID.

Line 502 forwards args.alias verbatim into chat_args.model. For llama.cpp, --alias may be comma-separated, but chat requests should carry one model identifier; otherwise model matching can fail.

💡 Proposed fix
-        chat_args.model = getattr(args, 'alias', None) or self.model_alias
+        alias = getattr(args, "alias", None)
+        if isinstance(alias, str) and getattr(args, "runtime", "") == "llama.cpp" and "," in alias:
+            alias = alias.split(",", 1)[0].strip()
+        chat_args.model = alias or self.model_alias
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ramalama/transports/base.py` at line 502, The assignment to chat_args.model
should normalize args.alias when it may contain multiple comma-separated
aliases; update the logic that sets chat_args.model (currently using
getattr(args, 'alias', None) or self.model_alias) to split the alias string on
commas, strip whitespace, and pick the first non-empty token (falling back to
self.model_alias if none), so chat requests always carry a single model
identifier.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid-ish; issue is vLLM parses --served-model-name differently, where model1,model2' will be treated as the model name (to do multiple model names in vLLM, you must do --served-model-name model1 model2instead. As such, doing a llama.cpp fix here will breakvLLM`. Then again, this field does not seem to matter in tests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Christopher-Chianelli, good point — my proposed fix already gates on getattr(args, "runtime", "") == "llama.cpp" specifically to avoid touching the vLLM path, so it wouldn't cause the comma-as-literal-name breakage you described for vLLM.

That said, your concern about args.runtime reliability is valid — if that attribute isn't consistently set when _connect_and_chat is called, the guard itself could silently mis-fire. And since you confirm the field doesn't appear to affect test outcomes currently, this is a low-priority concern.

Two options worth considering:

  1. Leave as-is for now, and track the multi-alias normalization as a follow-up. If someone actually uses a comma-separated llama.cpp alias and then tries to use the run chat mode, they'd see a model-not-found error — which is a real but edge-case failure.

  2. Apply a minimal, runtime-agnostic fix — always take the first comma-separated token, since neither llama.cpp nor vLLM would register "alias1,alias2" as a single valid model name in the models endpoint:

    alias = getattr(args, "alias", None)
    if alias and "," in alias:
        alias = alias.split(",", 1)[0].strip()
    chat_args.model = alias or self.model_alias

    This is safe for vLLM too, since vLLM would never accept a comma-joined string as a valid registered model name anyway.

Would you like to open a follow-up issue for this, or apply option 2 here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CodeRabbit runtimes are plugins, you must not place runtime specific conditionals in the core modules

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olliewalsh, understood and noted — runtime-specific conditionals belong in the plugin layer, not in core modules. My earlier suggestion gating on "llama.cpp" was incorrect.

The only valid fix here would be the runtime-agnostic approach: always normalize the alias by taking the first comma-separated token, since no runtime registers a comma-joined string as a valid model name:

alias = getattr(args, "alias", None)
if alias and "," in alias:
    alias = alias.split(",", 1)[0].strip()
chat_args.model = alias or self.model_alias

Given Christopher-Chianelli's note that this field doesn't appear to affect current tests, I'd suggest applying this normalization here or tracking it as a follow-up issue. Would you like me to open a follow-up issue for this?

(ᵔ◡ᔸ)🐇 Noted — keeping the core clean of plugin concerns.


✏️ Learnings added
Learnt from: olliewalsh
Repo: containers/ramalama PR: 2615
File: ramalama/transports/base.py:502-502
Timestamp: 2026-04-15T14:38:52.257Z
Learning: In the containers/ramalama repository, runtimes are implemented as plugins. Runtime-specific conditionals (e.g., checking for "llama.cpp", "vllm", etc.) must never be placed in core modules such as `ramalama/transports/base.py`. Runtime-specific logic must live in the plugin layer (e.g., under `ramalama/plugins/runtimes/`).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: olliewalsh
Repo: containers/ramalama PR: 2627
File: test/e2e/test_run.py:300-303
Timestamp: 2026-04-15T14:34:25.915Z
Learning: In this repo’s Python 3.10+ compatibility layer, always import `NamedTemporaryFile` from `ramalama.compat` (not from `tempfile`) so that the `delete_on_close` behavior is correctly backported for Python 3.10/3.11. When you need the temporary file to remain accessible by filesystem path within the `with` block, pass `delete_on_close=False` to `NamedTemporaryFile`. Use this pattern consistently (as in `test/unit/test_inference_engine_plugins.py`, `test/unit/test_common.py`, `ramalama/engine.py`, and `ramalama/stack.py`) to avoid Python-version-specific failures.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to pass the value through to llama.cpp/vllm verbatim

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread
olliewalsh marked this conversation as resolved.

if args.container:
return self._handle_container_chat(chat_args, server_process)
Expand Down
22 changes: 22 additions & 0 deletions test/e2e/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from subprocess import STDOUT, CalledProcessError

import pytest
import requests
import yaml

from test.conftest import (
Expand Down Expand Up @@ -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]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests already cover the llama-server cli. Could unit tests cover the model name used in the chat http requests too? If so then I don't think an expensive e2e test is really necessary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a unit test for the model name in http requests: https://github.com/Christopher-Chianelli/ramalama/blob/54afab654a65c11ae9633b3cb457a05ea7d8dd7a/test/unit/test_engine.py#L193-L210 ;

This e2e is more so to check llama.cpp does not break backwards compatibility (i.e. rename the alias argument, change the format of the /models endpoint, etc.

ctx.check_call(serve_cmd)
try:
ps_list = ctx.check_output(["ramalama", "ps", "--format", "{{.Names}} {{.Ports}}"])
port = re.search(rf"{container_id}.*->(?P<port>\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
Expand Down
20 changes: 20 additions & 0 deletions test/unit/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,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",
[
Expand Down
35 changes: 35 additions & 0 deletions test/unit/test_inference_engine_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def make_ns(
max_tokens=0,
port="8080",
host="::",
alias=None,
logfile=None,
debug=False,
webui="on",
Expand All @@ -56,6 +57,7 @@ def make_ns(
max_tokens=max_tokens,
port=port,
host=host,
alias=alias,
logfile=logfile,
debug=debug,
webui=webui,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
Loading