From d8d2d766219c3c304ffb9ddd6441cf732789cee3 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Fri, 4 Sep 2026 13:47:46 +0100 Subject: [PATCH] feat: add llmman as a local model provider llmman (https://github.com/llmmanorg/llmman) serves the Ollama API on port 17434, so the Ollama provider can drive it unchanged. Add a Llmman subclass of Ollama (port 17434, default model gemma4), register it next to Ollama, document it in the README and mirror the Ollama tests. The Ollama error message now uses self.name. --- README.md | 12 +++++- src/shelloracle/providers/__init__.py | 2 + src/shelloracle/providers/llmman.py | 13 +++++++ src/shelloracle/providers/ollama.py | 2 +- tests/providers/test_llmman.py | 54 +++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/shelloracle/providers/llmman.py create mode 100644 tests/providers/test_llmman.py diff --git a/README.md b/README.md index dbf079c..2d55d54 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

ShellOracle is an innovative terminal utility designed for intelligent shell command generation, bringing a new level of -efficiency to your command-line interactions. ShellOracle currently supports Ollama, OpenAI, Deepseek, LocalAI, and Grok! +efficiency to your command-line interactions. ShellOracle currently supports Ollama, llmman, OpenAI, Deepseek, LocalAI, and Grok! ![ShellOracle](https://i.imgur.com/lqTW1lO.gif) @@ -93,6 +93,16 @@ ollama pull qwen2.5-coder Refer to the [Ollama docs](https://ollama.ai) for installation, available models, and usage. +### llmman + +[llmman](https://github.com/llmmanorg/llmman) serves the Ollama API on port 17434. Start it and pull the model you +chose in the configure step. For example, if you chose `gemma4`, run: + +```shell +llmman serve +llmman pull gemma4 +``` + ### OpenAI To use ShellOracle with OpenAI's models, create an [API key](https://platform.openai.com/account/api-keys). Edit diff --git a/src/shelloracle/providers/__init__.py b/src/shelloracle/providers/__init__.py index 57e446d..ee1aa54 100644 --- a/src/shelloracle/providers/__init__.py +++ b/src/shelloracle/providers/__init__.py @@ -84,6 +84,7 @@ def __get__(self, instance: Provider, owner: type[Provider]) -> T: def _providers() -> dict[str, type[Provider]]: from shelloracle.providers.deepseek import Deepseek from shelloracle.providers.google import Google + from shelloracle.providers.llmman import Llmman from shelloracle.providers.localai import LocalAI from shelloracle.providers.ollama import Ollama from shelloracle.providers.openai import OpenAI @@ -92,6 +93,7 @@ def _providers() -> dict[str, type[Provider]]: return { Ollama.name: Ollama, + Llmman.name: Llmman, OpenAI.name: OpenAI, OpenAICompat.name: OpenAICompat, LocalAI.name: LocalAI, diff --git a/src/shelloracle/providers/llmman.py b/src/shelloracle/providers/llmman.py new file mode 100644 index 0000000..f0df8de --- /dev/null +++ b/src/shelloracle/providers/llmman.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from shelloracle.providers import Setting +from shelloracle.providers.ollama import Ollama + + +class Llmman(Ollama): + """llmman (https://github.com/llmmanorg/llmman) serves the Ollama API on port 17434.""" + + name = "llmman" + + port = Setting(default=17434) + model = Setting(default="gemma4") diff --git a/src/shelloracle/providers/ollama.py b/src/shelloracle/providers/ollama.py index 8fbc752..fbc83b9 100644 --- a/src/shelloracle/providers/ollama.py +++ b/src/shelloracle/providers/ollama.py @@ -77,5 +77,5 @@ async def generate(self, prompt: str) -> AsyncIterator[str]: raise ProviderError(response["error"]) yield response["response"] except (httpx.HTTPError, httpx.StreamError) as e: - msg = f"Something went wrong while querying Ollama: {e}" + msg = f"Something went wrong while querying {self.name}: {e}" raise ProviderError(msg) from e diff --git a/tests/providers/test_llmman.py b/tests/providers/test_llmman.py new file mode 100644 index 0000000..c9e5239 --- /dev/null +++ b/tests/providers/test_llmman.py @@ -0,0 +1,54 @@ +import pytest +from pytest_httpx import IteratorStream + +from shelloracle.config import Configuration +from shelloracle.providers.llmman import Llmman + + +class TestLlmman: + @pytest.fixture + def llmman_config(self): + config = { + "shelloracle": {"provider": "llmman"}, + "provider": {"llmman": {"host": "localhost", "port": 17434, "model": "gemma4"}}, + } + return Configuration(config) + + @pytest.fixture + def llmman_instance(self, llmman_config): + return Llmman(llmman_config) + + def test_name(self): + assert Llmman.name == "llmman" + + def test_host(self, llmman_instance): + assert llmman_instance.host == "localhost" + + def test_port(self, llmman_instance): + assert llmman_instance.port == 17434 + + def test_model(self, llmman_instance): + assert llmman_instance.model == "gemma4" + + def test_endpoint(self, llmman_instance): + assert llmman_instance.endpoint == "http://localhost:17434/api/generate" + + def test_defaults(self): + instance = Llmman(Configuration({"shelloracle": {"provider": "llmman"}, "provider": {}})) + assert instance.endpoint == "http://localhost:17434/api/generate" + assert instance.model == "gemma4" + + @pytest.mark.asyncio + async def test_generate(self, llmman_instance, httpx_mock): + responses = [ + b'{"response": "cat"}\n', + b'{"response": " test"}\n', + b'{"response": "."}\n', + b'{"response": "py"}\n', + b'{"response": ""}\n', + ] + httpx_mock.add_response(stream=IteratorStream(responses)) + result = "" + async for response in llmman_instance.generate(""): + result += response + assert result == "cat test.py"