From c1b991a5ffc4af3bba593c36cbe99e9bd307cd49 Mon Sep 17 00:00:00 2001 From: Unmilan Mukherjee Date: Tue, 30 Jun 2026 08:28:47 +0000 Subject: [PATCH] feat(client): add exists() method to Client and AsyncClient Add exists(model) that delegates to show() and returns True when the model is present, False on ResponseError with status 404, and re-raises other errors. Includes unit tests for sync/async clients covering success, 404, non-404 errors, and connection failures. Co-authored-by: Unmilan Mukherjee --- ollama/_client.py | 28 ++++++++++++ tests/test_client.py | 102 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/ollama/_client.py b/ollama/_client.py index 18cb0fb4..133db40b 100644 --- a/ollama/_client.py +++ b/ollama/_client.py @@ -664,6 +664,20 @@ def show(self, model: str) -> ShowResponse: ).model_dump(exclude_none=True), ) + def exists(self, model: str) -> bool: + """ + Returns True if the model exists, False if it does not. + + Raises `ResponseError` for errors other than 404. + """ + try: + self.show(model) + return True + except ResponseError as e: + if e.status_code == 404: + return False + raise + def ps(self) -> ProcessResponse: return self._request( ProcessResponse, @@ -1305,6 +1319,20 @@ async def show(self, model: str) -> ShowResponse: ).model_dump(exclude_none=True), ) + async def exists(self, model: str) -> bool: + """ + Returns True if the model exists, False if it does not. + + Raises `ResponseError` for errors other than 404. + """ + try: + await self.show(model) + return True + except ResponseError as e: + if e.status_code == 404: + return False + raise + async def ps(self) -> ProcessResponse: return await self._request( ProcessResponse, diff --git a/tests/test_client.py b/tests/test_client.py index 34657513..434965cd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -13,7 +13,7 @@ from werkzeug.wrappers import Request, Response from ollama._client import CONNECTION_ERROR_MESSAGE, AsyncClient, Client, _copy_tools -from ollama._types import Image, Message +from ollama._types import Image, Message, ResponseError PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYGAAAAAEAAH2FzhVAAAAAElFTkSuQmCC' PNG_BYTES = base64.b64decode(PNG_BASE64) @@ -879,6 +879,55 @@ def test_client_copy(httpserver: HTTPServer): assert response['status'] == 'success' +def test_client_exists_true(httpserver: HTTPServer): + httpserver.expect_ordered_request( + '/api/show', + method='POST', + json={'model': 'llama3.2'}, + ).respond_with_json( + { + 'modelfile': '# Modelfile', + 'template': '{{ .Prompt }}', + } + ) + + client = Client(httpserver.url_for('/')) + assert client.exists('llama3.2') is True + + +def test_client_exists_false(httpserver: HTTPServer): + httpserver.expect_ordered_request( + '/api/show', + method='POST', + json={'model': 'missing-model'}, + ).respond_with_json({'error': 'model not found'}, status=404) + + client = Client(httpserver.url_for('/')) + assert client.exists('missing-model') is False + + +def test_client_exists_raises_on_non_404_error(httpserver: HTTPServer): + httpserver.expect_ordered_request( + '/api/show', + method='POST', + json={'model': 'broken-model'}, + ).respond_with_json({'error': 'internal server error'}, status=500) + + client = Client(httpserver.url_for('/')) + with pytest.raises(ResponseError) as exc_info: + client.exists('broken-model') + + assert exc_info.value.status_code == 500 + assert exc_info.value.error == 'internal server error' + + +def test_client_exists_raises_on_connection_error(): + client = Client('http://localhost:1234') + + with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE): + client.exists('model') + + async def test_async_client_chat(httpserver: HTTPServer): httpserver.expect_ordered_request( '/api/chat', @@ -1256,6 +1305,57 @@ async def test_async_client_copy(httpserver: HTTPServer): assert response['status'] == 'success' +async def test_async_client_exists_true(httpserver: HTTPServer): + httpserver.expect_ordered_request( + '/api/show', + method='POST', + json={'model': 'llama3.2'}, + ).respond_with_json( + { + 'modelfile': '# Modelfile', + 'template': '{{ .Prompt }}', + } + ) + + client = AsyncClient(httpserver.url_for('/')) + assert await client.exists('llama3.2') is True + + +async def test_async_client_exists_false(httpserver: HTTPServer): + httpserver.expect_ordered_request( + '/api/show', + method='POST', + json={'model': 'missing-model'}, + ).respond_with_json({'error': 'model not found'}, status=404) + + client = AsyncClient(httpserver.url_for('/')) + assert await client.exists('missing-model') is False + + +async def test_async_client_exists_raises_on_non_404_error(httpserver: HTTPServer): + httpserver.expect_ordered_request( + '/api/show', + method='POST', + json={'model': 'broken-model'}, + ).respond_with_json({'error': 'internal server error'}, status=500) + + client = AsyncClient(httpserver.url_for('/')) + with pytest.raises(ResponseError) as exc_info: + await client.exists('broken-model') + + assert exc_info.value.status_code == 500 + assert exc_info.value.error == 'internal server error' + + +async def test_async_client_exists_raises_on_connection_error(): + client = AsyncClient('http://localhost:1234') + + with pytest.raises(ConnectionError) as exc_info: + await client.exists('model') + + assert str(exc_info.value) == CONNECTION_ERROR_MESSAGE + + def test_headers(): client = Client() assert client._client.headers['content-type'] == 'application/json'