mirror of
https://github.com/ollama/ollama-python.git
synced 2026-10-09 10:27:28 +00:00
request/response errors
This commit is contained in:
@@ -2,8 +2,11 @@ from ollama._client import Client, AsyncClient
|
|||||||
from ollama._types import (
|
from ollama._types import (
|
||||||
GenerateResponse,
|
GenerateResponse,
|
||||||
ChatResponse,
|
ChatResponse,
|
||||||
|
ProgressResponse,
|
||||||
Message,
|
Message,
|
||||||
Options,
|
Options,
|
||||||
|
RequestError,
|
||||||
|
ResponseError,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -11,8 +14,11 @@ __all__ = [
|
|||||||
'AsyncClient',
|
'AsyncClient',
|
||||||
'GenerateResponse',
|
'GenerateResponse',
|
||||||
'ChatResponse',
|
'ChatResponse',
|
||||||
|
'ProgressResponse',
|
||||||
'Message',
|
'Message',
|
||||||
'Options',
|
'Options',
|
||||||
|
'RequestError',
|
||||||
|
'ResponseError',
|
||||||
'generate',
|
'generate',
|
||||||
'chat',
|
'chat',
|
||||||
'pull',
|
'pull',
|
||||||
|
|||||||
+78
-20
@@ -16,7 +16,7 @@ if sys.version_info < (3, 9):
|
|||||||
else:
|
else:
|
||||||
from collections.abc import Iterator, AsyncIterator
|
from collections.abc import Iterator, AsyncIterator
|
||||||
|
|
||||||
from ollama._types import Message, Options
|
from ollama._types import Message, Options, RequestError, ResponseError
|
||||||
|
|
||||||
|
|
||||||
class BaseClient:
|
class BaseClient:
|
||||||
@@ -42,15 +42,26 @@ class Client(BaseClient):
|
|||||||
|
|
||||||
def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
|
def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
|
||||||
response = self._client.request(method, url, **kwargs)
|
response = self._client.request(method, url, **kwargs)
|
||||||
response.raise_for_status()
|
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
raise ResponseError(e.response.text, e.response.status_code) from None
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _stream(self, method: str, url: str, **kwargs) -> Iterator[Mapping[str, Any]]:
|
def _stream(self, method: str, url: str, **kwargs) -> Iterator[Mapping[str, Any]]:
|
||||||
with self._client.stream(method, url, **kwargs) as r:
|
with self._client.stream(method, url, **kwargs) as r:
|
||||||
|
try:
|
||||||
|
r.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
e.response.read()
|
||||||
|
raise ResponseError(e.response.text, e.response.status_code) from None
|
||||||
|
|
||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
partial = json.loads(line)
|
partial = json.loads(line)
|
||||||
if e := partial.get('error'):
|
if e := partial.get('error'):
|
||||||
raise Exception(e)
|
raise ResponseError(e)
|
||||||
yield partial
|
yield partial
|
||||||
|
|
||||||
def _request_stream(
|
def _request_stream(
|
||||||
@@ -75,11 +86,17 @@ class Client(BaseClient):
|
|||||||
options: Optional[Options] = None,
|
options: Optional[Options] = None,
|
||||||
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Create a response using the requested model.
|
||||||
|
|
||||||
|
Raises `RequestError` if a model is not provided.
|
||||||
|
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `GenerateResponse` if `stream` is `False`, otherwise returns a `GenerateResponse` generator.
|
Returns `GenerateResponse` if `stream` is `False`, otherwise returns a `GenerateResponse` generator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not model:
|
if not model:
|
||||||
raise Exception('must provide a model')
|
raise RequestError('must provide a model')
|
||||||
|
|
||||||
return self._request_stream(
|
return self._request_stream(
|
||||||
'POST',
|
'POST',
|
||||||
@@ -108,19 +125,25 @@ class Client(BaseClient):
|
|||||||
options: Optional[Options] = None,
|
options: Optional[Options] = None,
|
||||||
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Create a chat response using the requested model.
|
||||||
|
|
||||||
|
Raises `RequestError` if a model is not provided.
|
||||||
|
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ChatResponse` if `stream` is `False`, otherwise returns a `ChatResponse` generator.
|
Returns `ChatResponse` if `stream` is `False`, otherwise returns a `ChatResponse` generator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not model:
|
if not model:
|
||||||
raise Exception('must provide a model')
|
raise RequestError('must provide a model')
|
||||||
|
|
||||||
for message in messages or []:
|
for message in messages or []:
|
||||||
if not isinstance(message, dict):
|
if not isinstance(message, dict):
|
||||||
raise TypeError('messages must be a list of strings')
|
raise TypeError('messages must be a list of strings')
|
||||||
if not (role := message.get('role')) or role not in ['system', 'user', 'assistant']:
|
if not (role := message.get('role')) or role not in ['system', 'user', 'assistant']:
|
||||||
raise Exception('messages must contain a role and it must be one of "system", "user", or "assistant"')
|
raise RequestError('messages must contain a role and it must be one of "system", "user", or "assistant"')
|
||||||
if not message.get('content'):
|
if not message.get('content'):
|
||||||
raise Exception('messages must contain content')
|
raise RequestError('messages must contain content')
|
||||||
if images := message.get('images'):
|
if images := message.get('images'):
|
||||||
message['images'] = [_encode_image(image) for image in images]
|
message['images'] = [_encode_image(image) for image in images]
|
||||||
|
|
||||||
@@ -144,6 +167,8 @@ class Client(BaseClient):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||||
"""
|
"""
|
||||||
return self._request_stream(
|
return self._request_stream(
|
||||||
@@ -164,6 +189,8 @@ class Client(BaseClient):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||||
"""
|
"""
|
||||||
return self._request_stream(
|
return self._request_stream(
|
||||||
@@ -185,6 +212,8 @@ class Client(BaseClient):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], Iterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||||
"""
|
"""
|
||||||
if (realpath := _as_path(path)) and realpath.exists():
|
if (realpath := _as_path(path)) and realpath.exists():
|
||||||
@@ -192,7 +221,7 @@ class Client(BaseClient):
|
|||||||
elif modelfile:
|
elif modelfile:
|
||||||
modelfile = self._parse_modelfile(modelfile)
|
modelfile = self._parse_modelfile(modelfile)
|
||||||
else:
|
else:
|
||||||
raise Exception('must provide either path or modelfile')
|
raise RequestError('must provide either path or modelfile')
|
||||||
|
|
||||||
return self._request_stream(
|
return self._request_stream(
|
||||||
'POST',
|
'POST',
|
||||||
@@ -233,8 +262,8 @@ class Client(BaseClient):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self._request('HEAD', f'/api/blobs/{digest}')
|
self._request('HEAD', f'/api/blobs/{digest}')
|
||||||
except httpx.HTTPStatusError as e:
|
except ResponseError as e:
|
||||||
if e.response.status_code != 404:
|
if e.status_code != 404:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
with open(path, 'rb') as r:
|
with open(path, 'rb') as r:
|
||||||
@@ -263,16 +292,27 @@ class AsyncClient(BaseClient):
|
|||||||
|
|
||||||
async def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
|
async def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
|
||||||
response = await self._client.request(method, url, **kwargs)
|
response = await self._client.request(method, url, **kwargs)
|
||||||
response.raise_for_status()
|
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
raise ResponseError(e.response.text, e.response.status_code) from None
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def _stream(self, method: str, url: str, **kwargs) -> AsyncIterator[Mapping[str, Any]]:
|
async def _stream(self, method: str, url: str, **kwargs) -> AsyncIterator[Mapping[str, Any]]:
|
||||||
async def inner():
|
async def inner():
|
||||||
async with self._client.stream(method, url, **kwargs) as r:
|
async with self._client.stream(method, url, **kwargs) as r:
|
||||||
|
try:
|
||||||
|
r.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
e.response.read()
|
||||||
|
raise ResponseError(e.response.text, e.response.status_code) from None
|
||||||
|
|
||||||
async for line in r.aiter_lines():
|
async for line in r.aiter_lines():
|
||||||
partial = json.loads(line)
|
partial = json.loads(line)
|
||||||
if e := partial.get('error'):
|
if e := partial.get('error'):
|
||||||
raise Exception(e)
|
raise ResponseError(e)
|
||||||
yield partial
|
yield partial
|
||||||
|
|
||||||
return inner()
|
return inner()
|
||||||
@@ -303,10 +343,16 @@ class AsyncClient(BaseClient):
|
|||||||
options: Optional[Options] = None,
|
options: Optional[Options] = None,
|
||||||
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Create a response using the requested model.
|
||||||
|
|
||||||
|
Raises `RequestError` if a model is not provided.
|
||||||
|
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `GenerateResponse` if `stream` is `False`, otherwise returns an asynchronous `GenerateResponse` generator.
|
Returns `GenerateResponse` if `stream` is `False`, otherwise returns an asynchronous `GenerateResponse` generator.
|
||||||
"""
|
"""
|
||||||
if not model:
|
if not model:
|
||||||
raise Exception('must provide a model')
|
raise RequestError('must provide a model')
|
||||||
|
|
||||||
return await self._request_stream(
|
return await self._request_stream(
|
||||||
'POST',
|
'POST',
|
||||||
@@ -335,18 +381,24 @@ class AsyncClient(BaseClient):
|
|||||||
options: Optional[Options] = None,
|
options: Optional[Options] = None,
|
||||||
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Create a chat response using the requested model.
|
||||||
|
|
||||||
|
Raises `RequestError` if a model is not provided.
|
||||||
|
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ChatResponse` if `stream` is `False`, otherwise returns an asynchronous `ChatResponse` generator.
|
Returns `ChatResponse` if `stream` is `False`, otherwise returns an asynchronous `ChatResponse` generator.
|
||||||
"""
|
"""
|
||||||
if not model:
|
if not model:
|
||||||
raise Exception('must provide a model')
|
raise RequestError('must provide a model')
|
||||||
|
|
||||||
for message in messages or []:
|
for message in messages or []:
|
||||||
if not isinstance(message, dict):
|
if not isinstance(message, dict):
|
||||||
raise TypeError('messages must be a list of strings')
|
raise TypeError('messages must be a list of strings')
|
||||||
if not (role := message.get('role')) or role not in ['system', 'user', 'assistant']:
|
if not (role := message.get('role')) or role not in ['system', 'user', 'assistant']:
|
||||||
raise Exception('messages must contain a role and it must be one of "system", "user", or "assistant"')
|
raise RequestError('messages must contain a role and it must be one of "system", "user", or "assistant"')
|
||||||
if not message.get('content'):
|
if not message.get('content'):
|
||||||
raise Exception('messages must contain content')
|
raise RequestError('messages must contain content')
|
||||||
if images := message.get('images'):
|
if images := message.get('images'):
|
||||||
message['images'] = [_encode_image(image) for image in images]
|
message['images'] = [_encode_image(image) for image in images]
|
||||||
|
|
||||||
@@ -370,6 +422,8 @@ class AsyncClient(BaseClient):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||||
"""
|
"""
|
||||||
return await self._request_stream(
|
return await self._request_stream(
|
||||||
@@ -390,6 +444,8 @@ class AsyncClient(BaseClient):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||||
"""
|
"""
|
||||||
return await self._request_stream(
|
return await self._request_stream(
|
||||||
@@ -411,6 +467,8 @@ class AsyncClient(BaseClient):
|
|||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
) -> Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
|
Raises `ResponseError` if the request could not be fulfilled.
|
||||||
|
|
||||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||||
"""
|
"""
|
||||||
if (realpath := _as_path(path)) and realpath.exists():
|
if (realpath := _as_path(path)) and realpath.exists():
|
||||||
@@ -418,7 +476,7 @@ class AsyncClient(BaseClient):
|
|||||||
elif modelfile:
|
elif modelfile:
|
||||||
modelfile = await self._parse_modelfile(modelfile)
|
modelfile = await self._parse_modelfile(modelfile)
|
||||||
else:
|
else:
|
||||||
raise Exception('must provide either path or modelfile')
|
raise RequestError('must provide either path or modelfile')
|
||||||
|
|
||||||
return await self._request_stream(
|
return await self._request_stream(
|
||||||
'POST',
|
'POST',
|
||||||
@@ -459,8 +517,8 @@ class AsyncClient(BaseClient):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await self._request('HEAD', f'/api/blobs/{digest}')
|
await self._request('HEAD', f'/api/blobs/{digest}')
|
||||||
except httpx.HTTPStatusError as e:
|
except ResponseError as e:
|
||||||
if e.response.status_code != 404:
|
if e.status_code != 404:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def upload_bytes():
|
async def upload_bytes():
|
||||||
@@ -498,7 +556,7 @@ def _encode_image(image) -> str:
|
|||||||
elif b := _as_bytesio(image):
|
elif b := _as_bytesio(image):
|
||||||
b64 = b64encode(b.read())
|
b64 = b64encode(b.read())
|
||||||
else:
|
else:
|
||||||
raise Exception('images must be a list of bytes, path-like objects, or file-like objects')
|
raise RequestError('images must be a list of bytes, path-like objects, or file-like objects')
|
||||||
|
|
||||||
return b64.decode('utf-8')
|
return b64.decode('utf-8')
|
||||||
|
|
||||||
|
|||||||
@@ -80,3 +80,28 @@ class Options(TypedDict, total=False):
|
|||||||
mirostat_eta: float
|
mirostat_eta: float
|
||||||
penalize_newline: bool
|
penalize_newline: bool
|
||||||
stop: Sequence[str]
|
stop: Sequence[str]
|
||||||
|
|
||||||
|
|
||||||
|
class RequestError(Exception):
|
||||||
|
"""
|
||||||
|
Common class for request errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, content: str):
|
||||||
|
super().__init__(content)
|
||||||
|
self.content = content
|
||||||
|
"Reason for the error."
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseError(Exception):
|
||||||
|
"""
|
||||||
|
Common class for response errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, content: str, status_code: int = -1):
|
||||||
|
super().__init__(content)
|
||||||
|
self.content = content
|
||||||
|
"Reason for the error."
|
||||||
|
|
||||||
|
self.status_code = status_code
|
||||||
|
"HTTP status code of the response."
|
||||||
|
|||||||
Reference in New Issue
Block a user