mirror of
https://github.com/ollama/ollama-python.git
synced 2026-06-16 21:24:52 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0561f42701 | |||
| 7e64093df0 | |||
| f7e23ddb93 | |||
| 967fd657f1 | |||
| 12d6842f32 | |||
| 7a6ab044c6 | |||
| eefe5c9666 | |||
| 89e719ab92 | |||
| 02495ffd77 | |||
| 2cad1f5428 | |||
| d9746ead51 | |||
| c7d4f1674a | |||
| ba27e71a20 | |||
| fbf94d8660 | |||
| 2434443b88 | |||
| 2be0bcf92a | |||
| fc8585eabd | |||
| f5c8ee0a3e | |||
| a0388b2e32 | |||
| f718dab45d | |||
| 4dec73e8be | |||
| 4f9fb88137 | |||
| bee11029f7 | |||
| 427b0c6291 |
@@ -1,10 +1,10 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'examples/**'
|
||||
- '**/README.md'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -20,17 +20,27 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
- run: poetry install --with=dev
|
||||
- run: poetry run pytest . --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov=ollama --cov-report=xml --cov-report=html
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pytest-results-${{ matrix.python-version }}
|
||||
path: junit/test-results-${{ matrix.python-version }}.xml
|
||||
if: ${{ always() }}
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: pipx install poetry
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: poetry
|
||||
- run: poetry install --with=dev
|
||||
- run: poetry run ruff check --output-format=github .
|
||||
- run: poetry run ruff format --check .
|
||||
- run: poetry run pytest . --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov=ollama --cov-report=xml --cov-report=html
|
||||
- name: check poetry.lock is up-to-date
|
||||
run: poetry check --lock
|
||||
- name: check requirements.txt is up-to-date
|
||||
run: |
|
||||
poetry export >requirements.txt
|
||||
git diff --exit-code requirements.txt
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pytest-results-${{ matrix.python-version }}
|
||||
path: junit/test-results-${{ matrix.python-version }}.xml
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -129,12 +129,7 @@ ollama.show('llama3.2')
|
||||
### Create
|
||||
|
||||
```python
|
||||
modelfile='''
|
||||
FROM llama3.2
|
||||
SYSTEM You are mario from super mario bros.
|
||||
'''
|
||||
|
||||
ollama.create(model='example', modelfile=modelfile)
|
||||
ollama.create(model='example', from_='llama3.2', system="You are Mario from Super Mario Bros.")
|
||||
```
|
||||
|
||||
### Copy
|
||||
|
||||
+2
-4
@@ -6,6 +6,8 @@ Run the examples in this directory with:
|
||||
python3 examples/<example>.py
|
||||
```
|
||||
|
||||
See [ollama/docs/api.md](https://github.com/ollama/ollama/blob/main/docs/api.md) for full API documentation
|
||||
|
||||
### Chat - Chat with a model
|
||||
- [chat.py](chat.py)
|
||||
- [async-chat.py](async-chat.py)
|
||||
@@ -50,12 +52,8 @@ Requirement: `pip install tqdm`
|
||||
|
||||
|
||||
### Ollama Create - Create a model from a Modelfile
|
||||
```python
|
||||
python create.py <model> <modelfile>
|
||||
```
|
||||
- [create.py](create.py)
|
||||
|
||||
See [ollama/docs/modelfile.md](https://github.com/ollama/ollama/blob/main/docs/modelfile.md) for more information on the Modelfile format.
|
||||
|
||||
|
||||
### Ollama Embed - Generate embeddings with a model
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
from ollama import AsyncClient
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
import ollama
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
from ollama import AsyncClient
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ollama import AsyncClient
|
||||
|
||||
|
||||
# Define the schema for the response
|
||||
class FriendInfo(BaseModel):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
from ollama import ChatResponse
|
||||
|
||||
import ollama
|
||||
from ollama import ChatResponse
|
||||
|
||||
|
||||
def add_two_numbers(a: int, b: int) -> int:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from ollama import chat
|
||||
|
||||
|
||||
messages = [
|
||||
{
|
||||
'role': 'user',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from ollama import chat
|
||||
|
||||
|
||||
messages = [
|
||||
{
|
||||
'role': 'user',
|
||||
|
||||
Regular → Executable
+9
-29
@@ -1,30 +1,10 @@
|
||||
import sys
|
||||
from ollama import Client
|
||||
|
||||
from ollama import create
|
||||
|
||||
|
||||
args = sys.argv[1:]
|
||||
if len(args) == 2:
|
||||
# create from local file
|
||||
path = args[1]
|
||||
else:
|
||||
print('usage: python create.py <name> <filepath>')
|
||||
sys.exit(1)
|
||||
|
||||
# TODO: update to real Modelfile values
|
||||
modelfile = f"""
|
||||
FROM {path}
|
||||
"""
|
||||
example_modelfile = """
|
||||
FROM llama3.2
|
||||
# sets the temperature to 1 [higher is more creative, lower is more coherent]
|
||||
PARAMETER temperature 1
|
||||
# sets the context window size to 4096, this controls how many tokens the LLM can use as context to generate the next token
|
||||
PARAMETER num_ctx 4096
|
||||
|
||||
# sets a custom system message to specify the behavior of the chat assistant
|
||||
SYSTEM You are Mario from super mario bros, acting as an assistant.
|
||||
"""
|
||||
|
||||
for response in create(model=args[0], modelfile=modelfile, stream=True):
|
||||
print(response['status'])
|
||||
client = Client()
|
||||
response = client.create(
|
||||
model='my-assistant',
|
||||
from_='llama3.2',
|
||||
system='You are mario from Super Mario Bros.',
|
||||
stream=False,
|
||||
)
|
||||
print(response.status)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from ollama import generate
|
||||
|
||||
|
||||
for part in generate('llama3.2', 'Why is the sky blue?', stream=True):
|
||||
print(part['response'], end='', flush=True)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from ollama import generate
|
||||
|
||||
|
||||
response = generate('llama3.2', 'Why is the sky blue?')
|
||||
print(response['response'])
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
from ollama import list
|
||||
from ollama import ListResponse
|
||||
from ollama import ListResponse, list
|
||||
|
||||
response: ListResponse = list()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from ollama import chat
|
||||
|
||||
# from pathlib import Path
|
||||
|
||||
# Pass in the path to the image
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import sys
|
||||
import random
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
|
||||
from ollama import generate
|
||||
|
||||
|
||||
latest = httpx.get('https://xkcd.com/info.0.json')
|
||||
latest.raise_for_status()
|
||||
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
from ollama import ps, pull, chat
|
||||
from ollama import ProcessResponse
|
||||
from ollama import ProcessResponse, chat, ps, pull
|
||||
|
||||
# Ensure at least one model is loaded
|
||||
response = pull('llama3.2', stream=True)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
from tqdm import tqdm
|
||||
from ollama import pull
|
||||
|
||||
from ollama import pull
|
||||
|
||||
current_digest, bars = '', {}
|
||||
for progress in pull('llama3.2', stream=True):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ollama import chat
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from ollama import chat
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ollama import chat
|
||||
|
||||
|
||||
# Define the schema for the response
|
||||
class FriendInfo(BaseModel):
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
from ollama import chat
|
||||
from ollama import ChatResponse
|
||||
from ollama import ChatResponse, chat
|
||||
|
||||
|
||||
def add_two_numbers(a: int, b: int) -> int:
|
||||
|
||||
+19
-19
@@ -1,40 +1,40 @@
|
||||
from ollama._client import Client, AsyncClient
|
||||
from ollama._client import AsyncClient, Client
|
||||
from ollama._types import (
|
||||
Options,
|
||||
Message,
|
||||
Image,
|
||||
Tool,
|
||||
GenerateResponse,
|
||||
ChatResponse,
|
||||
EmbedResponse,
|
||||
EmbeddingsResponse,
|
||||
StatusResponse,
|
||||
ProgressResponse,
|
||||
EmbedResponse,
|
||||
GenerateResponse,
|
||||
Image,
|
||||
ListResponse,
|
||||
ShowResponse,
|
||||
Message,
|
||||
Options,
|
||||
ProcessResponse,
|
||||
ProgressResponse,
|
||||
RequestError,
|
||||
ResponseError,
|
||||
ShowResponse,
|
||||
StatusResponse,
|
||||
Tool,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'Client',
|
||||
'AsyncClient',
|
||||
'Options',
|
||||
'Message',
|
||||
'Image',
|
||||
'Tool',
|
||||
'GenerateResponse',
|
||||
'ChatResponse',
|
||||
'Client',
|
||||
'EmbedResponse',
|
||||
'EmbeddingsResponse',
|
||||
'StatusResponse',
|
||||
'ProgressResponse',
|
||||
'GenerateResponse',
|
||||
'Image',
|
||||
'ListResponse',
|
||||
'ShowResponse',
|
||||
'Message',
|
||||
'Options',
|
||||
'ProcessResponse',
|
||||
'ProgressResponse',
|
||||
'RequestError',
|
||||
'ResponseError',
|
||||
'ShowResponse',
|
||||
'StatusResponse',
|
||||
'Tool',
|
||||
]
|
||||
|
||||
_client = Client()
|
||||
|
||||
+97
-93
@@ -1,16 +1,17 @@
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import platform
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import urllib.parse
|
||||
from hashlib import sha256
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from hashlib import sha256
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Mapping,
|
||||
Optional,
|
||||
@@ -21,17 +22,14 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
import sys
|
||||
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
|
||||
|
||||
from ollama._utils import convert_function_to_tool
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing import Iterator, AsyncIterator
|
||||
from typing import AsyncIterator, Iterator
|
||||
else:
|
||||
from collections.abc import Iterator, AsyncIterator
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
|
||||
from importlib import metadata
|
||||
|
||||
@@ -45,13 +43,13 @@ import httpx
|
||||
from ollama._types import (
|
||||
ChatRequest,
|
||||
ChatResponse,
|
||||
CreateRequest,
|
||||
CopyRequest,
|
||||
CreateRequest,
|
||||
DeleteRequest,
|
||||
EmbedRequest,
|
||||
EmbedResponse,
|
||||
EmbeddingsRequest,
|
||||
EmbeddingsResponse,
|
||||
EmbedRequest,
|
||||
EmbedResponse,
|
||||
GenerateRequest,
|
||||
GenerateResponse,
|
||||
Image,
|
||||
@@ -62,7 +60,6 @@ from ollama._types import (
|
||||
ProgressResponse,
|
||||
PullRequest,
|
||||
PushRequest,
|
||||
RequestError,
|
||||
ResponseError,
|
||||
ShowRequest,
|
||||
ShowResponse,
|
||||
@@ -70,7 +67,6 @@ from ollama._types import (
|
||||
Tool,
|
||||
)
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
@@ -110,17 +106,22 @@ class BaseClient:
|
||||
)
|
||||
|
||||
|
||||
CONNECTION_ERROR_MESSAGE = 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'
|
||||
|
||||
|
||||
class Client(BaseClient):
|
||||
def __init__(self, host: Optional[str] = None, **kwargs) -> None:
|
||||
super().__init__(httpx.Client, host, **kwargs)
|
||||
|
||||
def _request_raw(self, *args, **kwargs):
|
||||
r = self._client.request(*args, **kwargs)
|
||||
try:
|
||||
r = self._client.request(*args, **kwargs)
|
||||
r.raise_for_status()
|
||||
return r
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise ResponseError(e.response.text, e.response.status_code) from None
|
||||
return r
|
||||
except httpx.ConnectError:
|
||||
raise ConnectionError(CONNECTION_ERROR_MESSAGE) from None
|
||||
|
||||
@overload
|
||||
def _request(
|
||||
@@ -476,10 +477,16 @@ class Client(BaseClient):
|
||||
def create(
|
||||
self,
|
||||
model: str,
|
||||
path: Optional[Union[str, PathLike]] = None,
|
||||
modelfile: Optional[str] = None,
|
||||
*,
|
||||
quantize: Optional[str] = None,
|
||||
from_: Optional[str] = None,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
adapters: Optional[Dict[str, str]] = None,
|
||||
template: Optional[str] = None,
|
||||
license: Optional[Union[str, List[str]]] = None,
|
||||
system: Optional[str] = None,
|
||||
parameters: Optional[Union[Mapping[str, Any], Options]] = None,
|
||||
messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,
|
||||
*,
|
||||
stream: Literal[False] = False,
|
||||
) -> ProgressResponse: ...
|
||||
|
||||
@@ -487,20 +494,32 @@ class Client(BaseClient):
|
||||
def create(
|
||||
self,
|
||||
model: str,
|
||||
path: Optional[Union[str, PathLike]] = None,
|
||||
modelfile: Optional[str] = None,
|
||||
*,
|
||||
quantize: Optional[str] = None,
|
||||
from_: Optional[str] = None,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
adapters: Optional[Dict[str, str]] = None,
|
||||
template: Optional[str] = None,
|
||||
license: Optional[Union[str, List[str]]] = None,
|
||||
system: Optional[str] = None,
|
||||
parameters: Optional[Union[Mapping[str, Any], Options]] = None,
|
||||
messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,
|
||||
*,
|
||||
stream: Literal[True] = True,
|
||||
) -> Iterator[ProgressResponse]: ...
|
||||
|
||||
def create(
|
||||
self,
|
||||
model: str,
|
||||
path: Optional[Union[str, PathLike]] = None,
|
||||
modelfile: Optional[str] = None,
|
||||
*,
|
||||
quantize: Optional[str] = None,
|
||||
from_: Optional[str] = None,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
adapters: Optional[Dict[str, str]] = None,
|
||||
template: Optional[str] = None,
|
||||
license: Optional[Union[str, List[str]]] = None,
|
||||
system: Optional[str] = None,
|
||||
parameters: Optional[Union[Mapping[str, Any], Options]] = None,
|
||||
messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
) -> Union[ProgressResponse, Iterator[ProgressResponse]]:
|
||||
"""
|
||||
@@ -508,45 +527,27 @@ class Client(BaseClient):
|
||||
|
||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||
"""
|
||||
if (realpath := _as_path(path)) and realpath.exists():
|
||||
modelfile = self._parse_modelfile(realpath.read_text(), base=realpath.parent)
|
||||
elif modelfile:
|
||||
modelfile = self._parse_modelfile(modelfile)
|
||||
else:
|
||||
raise RequestError('must provide either path or modelfile')
|
||||
|
||||
return self._request(
|
||||
ProgressResponse,
|
||||
'POST',
|
||||
'/api/create',
|
||||
json=CreateRequest(
|
||||
model=model,
|
||||
modelfile=modelfile,
|
||||
stream=stream,
|
||||
quantize=quantize,
|
||||
from_=from_,
|
||||
files=files,
|
||||
adapters=adapters,
|
||||
license=license,
|
||||
template=template,
|
||||
system=system,
|
||||
parameters=parameters,
|
||||
messages=messages,
|
||||
).model_dump(exclude_none=True),
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
def _parse_modelfile(self, modelfile: str, base: Optional[Path] = None) -> str:
|
||||
base = Path.cwd() if base is None else base
|
||||
|
||||
out = io.StringIO()
|
||||
for line in io.StringIO(modelfile):
|
||||
command, _, args = line.partition(' ')
|
||||
if command.upper() not in ['FROM', 'ADAPTER']:
|
||||
print(line, end='', file=out)
|
||||
continue
|
||||
|
||||
path = Path(args.strip()).expanduser()
|
||||
path = path if path.is_absolute() else base / path
|
||||
if path.exists():
|
||||
args = f'@{self._create_blob(path)}\n'
|
||||
print(command, args, end='', file=out)
|
||||
|
||||
return out.getvalue()
|
||||
|
||||
def _create_blob(self, path: Union[str, Path]) -> str:
|
||||
def create_blob(self, path: Union[str, Path]) -> str:
|
||||
sha256sum = sha256()
|
||||
with open(path, 'rb') as r:
|
||||
while True:
|
||||
@@ -558,7 +559,7 @@ class Client(BaseClient):
|
||||
digest = f'sha256:{sha256sum.hexdigest()}'
|
||||
|
||||
with open(path, 'rb') as r:
|
||||
self._request_raw('POST', f'/api/blobs/sha256:{digest}', content=r)
|
||||
self._request_raw('POST', f'/api/blobs/{digest}', content=r)
|
||||
|
||||
return digest
|
||||
|
||||
@@ -617,12 +618,14 @@ class AsyncClient(BaseClient):
|
||||
super().__init__(httpx.AsyncClient, host, **kwargs)
|
||||
|
||||
async def _request_raw(self, *args, **kwargs):
|
||||
r = await self._client.request(*args, **kwargs)
|
||||
try:
|
||||
r = await self._client.request(*args, **kwargs)
|
||||
r.raise_for_status()
|
||||
return r
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise ResponseError(e.response.text, e.response.status_code) from None
|
||||
return r
|
||||
except httpx.ConnectError:
|
||||
raise ConnectionError(CONNECTION_ERROR_MESSAGE) from None
|
||||
|
||||
@overload
|
||||
async def _request(
|
||||
@@ -978,31 +981,49 @@ class AsyncClient(BaseClient):
|
||||
async def create(
|
||||
self,
|
||||
model: str,
|
||||
path: Optional[Union[str, PathLike]] = None,
|
||||
modelfile: Optional[str] = None,
|
||||
*,
|
||||
quantize: Optional[str] = None,
|
||||
stream: Literal[False] = False,
|
||||
from_: Optional[str] = None,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
adapters: Optional[Dict[str, str]] = None,
|
||||
template: Optional[str] = None,
|
||||
license: Optional[Union[str, List[str]]] = None,
|
||||
system: Optional[str] = None,
|
||||
parameters: Optional[Union[Mapping[str, Any], Options]] = None,
|
||||
messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,
|
||||
*,
|
||||
stream: Literal[True] = True,
|
||||
) -> ProgressResponse: ...
|
||||
|
||||
@overload
|
||||
async def create(
|
||||
self,
|
||||
model: str,
|
||||
path: Optional[Union[str, PathLike]] = None,
|
||||
modelfile: Optional[str] = None,
|
||||
*,
|
||||
quantize: Optional[str] = None,
|
||||
from_: Optional[str] = None,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
adapters: Optional[Dict[str, str]] = None,
|
||||
template: Optional[str] = None,
|
||||
license: Optional[Union[str, List[str]]] = None,
|
||||
system: Optional[str] = None,
|
||||
parameters: Optional[Union[Mapping[str, Any], Options]] = None,
|
||||
messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,
|
||||
*,
|
||||
stream: Literal[True] = True,
|
||||
) -> AsyncIterator[ProgressResponse]: ...
|
||||
|
||||
async def create(
|
||||
self,
|
||||
model: str,
|
||||
path: Optional[Union[str, PathLike]] = None,
|
||||
modelfile: Optional[str] = None,
|
||||
*,
|
||||
quantize: Optional[str] = None,
|
||||
from_: Optional[str] = None,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
adapters: Optional[Dict[str, str]] = None,
|
||||
template: Optional[str] = None,
|
||||
license: Optional[Union[str, List[str]]] = None,
|
||||
system: Optional[str] = None,
|
||||
parameters: Optional[Union[Mapping[str, Any], Options]] = None,
|
||||
messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
) -> Union[ProgressResponse, AsyncIterator[ProgressResponse]]:
|
||||
"""
|
||||
@@ -1010,12 +1031,6 @@ class AsyncClient(BaseClient):
|
||||
|
||||
Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.
|
||||
"""
|
||||
if (realpath := _as_path(path)) and realpath.exists():
|
||||
modelfile = await self._parse_modelfile(realpath.read_text(), base=realpath.parent)
|
||||
elif modelfile:
|
||||
modelfile = await self._parse_modelfile(modelfile)
|
||||
else:
|
||||
raise RequestError('must provide either path or modelfile')
|
||||
|
||||
return await self._request(
|
||||
ProgressResponse,
|
||||
@@ -1023,32 +1038,21 @@ class AsyncClient(BaseClient):
|
||||
'/api/create',
|
||||
json=CreateRequest(
|
||||
model=model,
|
||||
modelfile=modelfile,
|
||||
stream=stream,
|
||||
quantize=quantize,
|
||||
from_=from_,
|
||||
files=files,
|
||||
adapters=adapters,
|
||||
license=license,
|
||||
template=template,
|
||||
system=system,
|
||||
parameters=parameters,
|
||||
messages=messages,
|
||||
).model_dump(exclude_none=True),
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
async def _parse_modelfile(self, modelfile: str, base: Optional[Path] = None) -> str:
|
||||
base = Path.cwd() if base is None else base
|
||||
|
||||
out = io.StringIO()
|
||||
for line in io.StringIO(modelfile):
|
||||
command, _, args = line.partition(' ')
|
||||
if command.upper() not in ['FROM', 'ADAPTER']:
|
||||
print(line, end='', file=out)
|
||||
continue
|
||||
|
||||
path = Path(args.strip()).expanduser()
|
||||
path = path if path.is_absolute() else base / path
|
||||
if path.exists():
|
||||
args = f'@{await self._create_blob(path)}\n'
|
||||
print(command, args, end='', file=out)
|
||||
|
||||
return out.getvalue()
|
||||
|
||||
async def _create_blob(self, path: Union[str, Path]) -> str:
|
||||
async def create_blob(self, path: Union[str, Path]) -> str:
|
||||
sha256sum = sha256()
|
||||
with open(path, 'rb') as r:
|
||||
while True:
|
||||
|
||||
+22
-8
@@ -1,11 +1,8 @@
|
||||
import json
|
||||
from base64 import b64decode, b64encode
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Any, Mapping, Optional, Union, Sequence
|
||||
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
from typing_extensions import Annotated, Literal
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Union
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
@@ -14,6 +11,8 @@ from pydantic import (
|
||||
Field,
|
||||
model_serializer,
|
||||
)
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
from typing_extensions import Annotated, Literal
|
||||
|
||||
|
||||
class SubscriptableBaseModel(BaseModel):
|
||||
@@ -401,13 +400,25 @@ class PushRequest(BaseStreamableRequest):
|
||||
|
||||
|
||||
class CreateRequest(BaseStreamableRequest):
|
||||
@model_serializer(mode='wrap')
|
||||
def serialize_model(self, nxt):
|
||||
output = nxt(self)
|
||||
if 'from_' in output:
|
||||
output['from'] = output.pop('from_')
|
||||
return output
|
||||
|
||||
"""
|
||||
Request to create a new model.
|
||||
"""
|
||||
|
||||
modelfile: Optional[str] = None
|
||||
|
||||
quantize: Optional[str] = None
|
||||
from_: Optional[str] = None
|
||||
files: Optional[Dict[str, str]] = None
|
||||
adapters: Optional[Dict[str, str]] = None
|
||||
template: Optional[str] = None
|
||||
license: Optional[Union[str, List[str]]] = None
|
||||
system: Optional[str] = None
|
||||
parameters: Optional[Union[Mapping[str, Any], Options]] = None
|
||||
messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None
|
||||
|
||||
|
||||
class ModelDetails(SubscriptableBaseModel):
|
||||
@@ -524,3 +535,6 @@ class ResponseError(Exception):
|
||||
|
||||
self.status_code = status_code
|
||||
'HTTP status code of the response.'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.error} (status code: {self.status_code})'
|
||||
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
|
||||
import inspect
|
||||
from typing import Callable, Union
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from typing import Callable, Union
|
||||
|
||||
import pydantic
|
||||
|
||||
from ollama._types import Tool
|
||||
|
||||
|
||||
|
||||
Generated
+53
-26
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -6,6 +6,7 @@ version = "0.7.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
@@ -20,6 +21,7 @@ version = "4.5.2"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"},
|
||||
{file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"},
|
||||
@@ -42,6 +44,7 @@ version = "2024.8.30"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
|
||||
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
|
||||
@@ -53,6 +56,8 @@ version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
@@ -64,6 +69,7 @@ version = "7.6.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
|
||||
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
|
||||
@@ -151,6 +157,8 @@ version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
@@ -165,6 +173,7 @@ version = "0.14.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
@@ -176,6 +185,7 @@ version = "1.0.6"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"},
|
||||
{file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"},
|
||||
@@ -193,13 +203,14 @@ trio = ["trio (>=0.22.0,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.2"
|
||||
version = "0.28.1"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -207,7 +218,6 @@ anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
@@ -222,6 +232,7 @@ version = "3.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
@@ -236,6 +247,7 @@ version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
@@ -247,6 +259,7 @@ version = "2.1.5"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||
@@ -316,6 +329,7 @@ version = "24.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
@@ -327,6 +341,7 @@ version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
@@ -342,6 +357,7 @@ version = "2.9.2"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
|
||||
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
|
||||
@@ -365,6 +381,7 @@ version = "2.23.4"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
|
||||
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
|
||||
@@ -466,6 +483,7 @@ version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
@@ -488,6 +506,7 @@ version = "0.24.0"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
|
||||
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
|
||||
@@ -506,6 +525,7 @@ version = "5.0.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
|
||||
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
|
||||
@@ -524,6 +544,7 @@ version = "1.1.0"
|
||||
description = "pytest-httpserver is a httpserver for pytest"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest_httpserver-1.1.0-py3-none-any.whl", hash = "sha256:7ef88be8ed3354b6784daa3daa75a422370327c634053cefb124903fa8d73a41"},
|
||||
{file = "pytest_httpserver-1.1.0.tar.gz", hash = "sha256:6b1cb0199e2ed551b1b94d43f096863bbf6ae5bcd7c75c2c06845e5ce2dc8701"},
|
||||
@@ -534,29 +555,30 @@ Werkzeug = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.7.4"
|
||||
version = "0.9.1"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478"},
|
||||
{file = "ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63"},
|
||||
{file = "ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20"},
|
||||
{file = "ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109"},
|
||||
{file = "ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452"},
|
||||
{file = "ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea"},
|
||||
{file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7"},
|
||||
{file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05"},
|
||||
{file = "ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06"},
|
||||
{file = "ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc"},
|
||||
{file = "ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172"},
|
||||
{file = "ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a"},
|
||||
{file = "ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd"},
|
||||
{file = "ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a"},
|
||||
{file = "ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac"},
|
||||
{file = "ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6"},
|
||||
{file = "ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f"},
|
||||
{file = "ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2"},
|
||||
{file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"},
|
||||
{file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"},
|
||||
{file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"},
|
||||
{file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"},
|
||||
{file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -565,6 +587,7 @@ version = "1.3.1"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
@@ -576,6 +599,8 @@ version = "2.0.2"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_full_version <= \"3.11.0a6\""
|
||||
files = [
|
||||
{file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
|
||||
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
|
||||
@@ -587,6 +612,7 @@ version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
@@ -598,6 +624,7 @@ version = "3.0.6"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"},
|
||||
{file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"},
|
||||
@@ -610,6 +637,6 @@ MarkupSafe = ">=2.1.1"
|
||||
watchdog = ["watchdog (>=2.3)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "8e93767305535b0a02f0d724edf1249fd928ff1021644eb9dc26dbfa191f6971"
|
||||
content-hash = "ff70c562172e38058111703d8219e18a0ed367e8e3ff647d7eff457d3bf3204e"
|
||||
|
||||
+15
-4
@@ -10,15 +10,18 @@ repository = "https://github.com/ollama/ollama-python"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
httpx = "^0.27.0"
|
||||
httpx = ">=0.27,<0.29"
|
||||
pydantic = "^2.9.0"
|
||||
|
||||
[tool.poetry.requires-plugins]
|
||||
poetry-plugin-export = ">=1.8"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = ">=7.4.3,<9.0.0"
|
||||
pytest-asyncio = ">=0.23.2,<0.25.0"
|
||||
pytest-cov = ">=4.1,<6.0"
|
||||
pytest-httpserver = "^1.0.8"
|
||||
ruff = ">=0.1.8,<0.8.0"
|
||||
ruff = ">=0.9.1,<0.10.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
@@ -33,8 +36,16 @@ quote-style = "single"
|
||||
indent-style = "space"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "B"]
|
||||
ignore = ["E501"]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"F", # pyflakes
|
||||
"B", # bugbear (likely bugs)
|
||||
"I", # sort imports
|
||||
"RUF022", # sort __all__
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = '--doctest-modules --ignore examples'
|
||||
|
||||
+3
-3
@@ -16,9 +16,9 @@ h11==0.14.0 ; python_version >= "3.8" and python_version < "4.0" \
|
||||
httpcore==1.0.6 ; python_version >= "3.8" and python_version < "4.0" \
|
||||
--hash=sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f \
|
||||
--hash=sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f
|
||||
httpx==0.27.2 ; python_version >= "3.8" and python_version < "4.0" \
|
||||
--hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \
|
||||
--hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2
|
||||
httpx==0.28.1 ; python_version >= "3.8" and python_version < "4.0" \
|
||||
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
|
||||
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
|
||||
idna==3.10 ; python_version >= "3.8" and python_version < "4.0" \
|
||||
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
|
||||
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||
|
||||
+90
-228
@@ -1,14 +1,16 @@
|
||||
import base64
|
||||
import os
|
||||
import json
|
||||
from pydantic import ValidationError, BaseModel
|
||||
import pytest
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pytest_httpserver import HTTPServer, URIPattern
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
from ollama._client import Client, AsyncClient, _copy_tools
|
||||
from ollama._client import CONNECTION_ERROR_MESSAGE, AsyncClient, Client, _copy_tools
|
||||
|
||||
PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYGAAAAAEAAH2FzhVAAAAAElFTkSuQmCC'
|
||||
PNG_BYTES = base64.b64decode(PNG_BASE64)
|
||||
@@ -91,7 +93,7 @@ def test_client_chat_stream(httpserver: HTTPServer):
|
||||
@pytest.mark.parametrize('message_format', ('dict', 'pydantic_model'))
|
||||
@pytest.mark.parametrize('file_style', ('path', 'bytes'))
|
||||
def test_client_chat_images(httpserver: HTTPServer, message_format: str, file_style: str, tmp_path):
|
||||
from ollama._types import Message, Image
|
||||
from ollama._types import Image, Message
|
||||
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/chat',
|
||||
@@ -536,52 +538,6 @@ def test_client_push_stream(httpserver: HTTPServer):
|
||||
assert part['status'] == next(it)
|
||||
|
||||
|
||||
def test_client_create_path(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as modelfile:
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
modelfile.write(f'FROM {blob.name}'.encode('utf-8'))
|
||||
modelfile.flush()
|
||||
|
||||
response = client.create('dummy', path=modelfile.name)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
def test_client_create_path_relative(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as modelfile:
|
||||
with tempfile.NamedTemporaryFile(dir=Path(modelfile.name).parent) as blob:
|
||||
modelfile.write(f'FROM {Path(blob.name).name}'.encode('utf-8'))
|
||||
modelfile.flush()
|
||||
|
||||
response = client.create('dummy', path=modelfile.name)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def userhomedir():
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
@@ -591,92 +547,56 @@ def userhomedir():
|
||||
os.environ['HOME'] = home
|
||||
|
||||
|
||||
def test_client_create_path_user_home(httpserver: HTTPServer, userhomedir):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
def test_client_create_with_blob(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'files': {'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as modelfile:
|
||||
with tempfile.NamedTemporaryFile(dir=userhomedir) as blob:
|
||||
modelfile.write(f'FROM ~/{Path(blob.name).name}'.encode('utf-8'))
|
||||
modelfile.flush()
|
||||
|
||||
response = client.create('dummy', path=modelfile.name)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
def test_client_create_modelfile(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
response = client.create('dummy', modelfile=f'FROM {blob.name}')
|
||||
with tempfile.NamedTemporaryFile():
|
||||
response = client.create('dummy', files={'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'})
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
def test_client_create_modelfile_roundtrip(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
def test_client_create_with_parameters_roundtrip(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': '''FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
TEMPLATE """[INST] <<SYS>>{{.System}}<</SYS>>
|
||||
{{.Prompt}} [/INST]"""
|
||||
SYSTEM """
|
||||
Use
|
||||
multiline
|
||||
strings.
|
||||
"""
|
||||
PARAMETER stop [INST]
|
||||
PARAMETER stop [/INST]
|
||||
PARAMETER stop <<SYS>>
|
||||
PARAMETER stop <</SYS>>''',
|
||||
'quantize': 'q4_k_m',
|
||||
'from': 'mymodel',
|
||||
'adapters': {'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},
|
||||
'template': '[INST] <<SYS>>{{.System}}<</SYS>>\n{{.Prompt}} [/INST]',
|
||||
'license': 'this is my license',
|
||||
'system': '\nUse\nmultiline\nstrings.\n',
|
||||
'parameters': {'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},
|
||||
'messages': [{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
with tempfile.NamedTemporaryFile():
|
||||
response = client.create(
|
||||
'dummy',
|
||||
modelfile='\n'.join(
|
||||
[
|
||||
f'FROM {blob.name}',
|
||||
'TEMPLATE """[INST] <<SYS>>{{.System}}<</SYS>>',
|
||||
'{{.Prompt}} [/INST]"""',
|
||||
'SYSTEM """',
|
||||
'Use',
|
||||
'multiline',
|
||||
'strings.',
|
||||
'"""',
|
||||
'PARAMETER stop [INST]',
|
||||
'PARAMETER stop [/INST]',
|
||||
'PARAMETER stop <<SYS>>',
|
||||
'PARAMETER stop <</SYS>>',
|
||||
]
|
||||
),
|
||||
quantize='q4_k_m',
|
||||
from_='mymodel',
|
||||
adapters={'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},
|
||||
template='[INST] <<SYS>>{{.System}}<</SYS>>\n{{.Prompt}} [/INST]',
|
||||
license='this is my license',
|
||||
system='\nUse\nmultiline\nstrings.\n',
|
||||
parameters={'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},
|
||||
messages=[{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],
|
||||
stream=False,
|
||||
)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
@@ -687,24 +607,24 @@ def test_client_create_from_library(httpserver: HTTPServer):
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM llama2',
|
||||
'from': 'llama2',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
response = client.create('dummy', modelfile='FROM llama2')
|
||||
response = client.create('dummy', from_='llama2')
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
def test_client_create_blob(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=201))
|
||||
httpserver.expect_ordered_request(re.compile('^/api/blobs/sha256[:-][0-9a-fA-F]{64}$'), method='POST').respond_with_response(Response(status=201))
|
||||
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
response = client._create_blob(blob.name)
|
||||
response = client.create_blob(blob.name)
|
||||
assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
|
||||
|
||||
@@ -714,7 +634,7 @@ def test_client_create_blob_exists(httpserver: HTTPServer):
|
||||
client = Client(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
response = client._create_blob(blob.name)
|
||||
response = client.create_blob(blob.name)
|
||||
assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
|
||||
|
||||
@@ -1015,142 +935,57 @@ async def test_async_client_push_stream(httpserver: HTTPServer):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_client_create_path(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
async def test_async_client_create_with_blob(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'files': {'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as modelfile:
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
modelfile.write(f'FROM {blob.name}'.encode('utf-8'))
|
||||
modelfile.flush()
|
||||
|
||||
response = await client.create('dummy', path=modelfile.name)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_client_create_path_relative(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as modelfile:
|
||||
with tempfile.NamedTemporaryFile(dir=Path(modelfile.name).parent) as blob:
|
||||
modelfile.write(f'FROM {Path(blob.name).name}'.encode('utf-8'))
|
||||
modelfile.flush()
|
||||
|
||||
response = await client.create('dummy', path=modelfile.name)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_client_create_path_user_home(httpserver: HTTPServer, userhomedir):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as modelfile:
|
||||
with tempfile.NamedTemporaryFile(dir=userhomedir) as blob:
|
||||
modelfile.write(f'FROM ~/{Path(blob.name).name}'.encode('utf-8'))
|
||||
modelfile.flush()
|
||||
|
||||
response = await client.create('dummy', path=modelfile.name)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_client_create_modelfile(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
response = await client.create('dummy', modelfile=f'FROM {blob.name}')
|
||||
with tempfile.NamedTemporaryFile():
|
||||
response = await client.create('dummy', files={'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'})
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_client_create_modelfile_roundtrip(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))
|
||||
async def test_async_client_create_with_parameters_roundtrip(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(
|
||||
'/api/create',
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': '''FROM @sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
TEMPLATE """[INST] <<SYS>>{{.System}}<</SYS>>
|
||||
{{.Prompt}} [/INST]"""
|
||||
SYSTEM """
|
||||
Use
|
||||
multiline
|
||||
strings.
|
||||
"""
|
||||
PARAMETER stop [INST]
|
||||
PARAMETER stop [/INST]
|
||||
PARAMETER stop <<SYS>>
|
||||
PARAMETER stop <</SYS>>''',
|
||||
'quantize': 'q4_k_m',
|
||||
'from': 'mymodel',
|
||||
'adapters': {'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},
|
||||
'template': '[INST] <<SYS>>{{.System}}<</SYS>>\n{{.Prompt}} [/INST]',
|
||||
'license': 'this is my license',
|
||||
'system': '\nUse\nmultiline\nstrings.\n',
|
||||
'parameters': {'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},
|
||||
'messages': [{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
with tempfile.NamedTemporaryFile():
|
||||
response = await client.create(
|
||||
'dummy',
|
||||
modelfile='\n'.join(
|
||||
[
|
||||
f'FROM {blob.name}',
|
||||
'TEMPLATE """[INST] <<SYS>>{{.System}}<</SYS>>',
|
||||
'{{.Prompt}} [/INST]"""',
|
||||
'SYSTEM """',
|
||||
'Use',
|
||||
'multiline',
|
||||
'strings.',
|
||||
'"""',
|
||||
'PARAMETER stop [INST]',
|
||||
'PARAMETER stop [/INST]',
|
||||
'PARAMETER stop <<SYS>>',
|
||||
'PARAMETER stop <</SYS>>',
|
||||
]
|
||||
),
|
||||
quantize='q4_k_m',
|
||||
from_='mymodel',
|
||||
adapters={'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},
|
||||
template='[INST] <<SYS>>{{.System}}<</SYS>>\n{{.Prompt}} [/INST]',
|
||||
license='this is my license',
|
||||
system='\nUse\nmultiline\nstrings.\n',
|
||||
parameters={'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},
|
||||
messages=[{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],
|
||||
stream=False,
|
||||
)
|
||||
assert response['status'] == 'success'
|
||||
|
||||
@@ -1162,25 +997,25 @@ async def test_async_client_create_from_library(httpserver: HTTPServer):
|
||||
method='POST',
|
||||
json={
|
||||
'model': 'dummy',
|
||||
'modelfile': 'FROM llama2',
|
||||
'from': 'llama2',
|
||||
'stream': False,
|
||||
},
|
||||
).respond_with_json({'status': 'success'})
|
||||
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
response = await client.create('dummy', modelfile='FROM llama2')
|
||||
response = await client.create('dummy', from_='llama2')
|
||||
assert response['status'] == 'success'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_client_create_blob(httpserver: HTTPServer):
|
||||
httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=201))
|
||||
httpserver.expect_ordered_request(re.compile('^/api/blobs/sha256[:-][0-9a-fA-F]{64}$'), method='POST').respond_with_response(Response(status=201))
|
||||
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
response = await client._create_blob(blob.name)
|
||||
response = await client.create_blob(blob.name)
|
||||
assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
|
||||
|
||||
@@ -1191,7 +1026,7 @@ async def test_async_client_create_blob_exists(httpserver: HTTPServer):
|
||||
client = AsyncClient(httpserver.url_for('/'))
|
||||
|
||||
with tempfile.NamedTemporaryFile() as blob:
|
||||
response = await client._create_blob(blob.name)
|
||||
response = await client.create_blob(blob.name)
|
||||
assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
|
||||
|
||||
@@ -1278,3 +1113,30 @@ def test_tool_validation():
|
||||
with pytest.raises(ValidationError):
|
||||
invalid_tool = {'type': 'invalid_type', 'function': {'name': 'test'}}
|
||||
list(_copy_tools([invalid_tool]))
|
||||
|
||||
|
||||
def test_client_connection_error():
|
||||
client = Client('http://localhost:1234')
|
||||
|
||||
with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):
|
||||
client.chat('model', messages=[{'role': 'user', 'content': 'prompt'}])
|
||||
with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):
|
||||
client.chat('model', messages=[{'role': 'user', 'content': 'prompt'}])
|
||||
with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):
|
||||
client.generate('model', 'prompt')
|
||||
with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):
|
||||
client.show('model')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_client_connection_error():
|
||||
client = AsyncClient('http://localhost:1234')
|
||||
with pytest.raises(ConnectionError) as exc_info:
|
||||
await client.chat('model', messages=[{'role': 'user', 'content': 'prompt'}])
|
||||
assert str(exc_info.value) == 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'
|
||||
with pytest.raises(ConnectionError) as exc_info:
|
||||
await client.generate('model', 'prompt')
|
||||
assert str(exc_info.value) == 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'
|
||||
with pytest.raises(ConnectionError) as exc_info:
|
||||
await client.show('model')
|
||||
assert str(exc_info.value) == 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import tempfile
|
||||
from base64 import b64encode
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from ollama._types import Image
|
||||
import tempfile
|
||||
|
||||
from ollama._types import CreateRequest, Image
|
||||
|
||||
|
||||
def test_image_serialization_bytes():
|
||||
@@ -52,3 +53,42 @@ def test_image_serialization_string_path():
|
||||
with pytest.raises(ValueError):
|
||||
img = Image(value='not an image')
|
||||
img.model_dump()
|
||||
|
||||
|
||||
def test_create_request_serialization():
|
||||
request = CreateRequest(model='test-model', from_='base-model', quantize='q4_0', files={'file1': 'content1'}, adapters={'adapter1': 'content1'}, template='test template', license='MIT', system='test system', parameters={'param1': 'value1'})
|
||||
|
||||
serialized = request.model_dump()
|
||||
assert serialized['from'] == 'base-model'
|
||||
assert 'from_' not in serialized
|
||||
assert serialized['quantize'] == 'q4_0'
|
||||
assert serialized['files'] == {'file1': 'content1'}
|
||||
assert serialized['adapters'] == {'adapter1': 'content1'}
|
||||
assert serialized['template'] == 'test template'
|
||||
assert serialized['license'] == 'MIT'
|
||||
assert serialized['system'] == 'test system'
|
||||
assert serialized['parameters'] == {'param1': 'value1'}
|
||||
|
||||
|
||||
def test_create_request_serialization_exclude_none_true():
|
||||
request = CreateRequest(model='test-model', from_=None, quantize=None)
|
||||
serialized = request.model_dump(exclude_none=True)
|
||||
assert serialized == {'model': 'test-model'}
|
||||
assert 'from' not in serialized
|
||||
assert 'from_' not in serialized
|
||||
assert 'quantize' not in serialized
|
||||
|
||||
|
||||
def test_create_request_serialization_exclude_none_false():
|
||||
request = CreateRequest(model='test-model', from_=None, quantize=None)
|
||||
serialized = request.model_dump(exclude_none=False)
|
||||
assert 'from' in serialized
|
||||
assert 'quantize' in serialized
|
||||
assert 'adapters' in serialized
|
||||
assert 'from_' not in serialized
|
||||
|
||||
|
||||
def test_create_request_serialization_license_list():
|
||||
request = CreateRequest(model='test-model', license=['MIT', 'Apache-2.0'])
|
||||
serialized = request.model_dump()
|
||||
assert serialized['license'] == ['MIT', 'Apache-2.0']
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ import json
|
||||
import sys
|
||||
from typing import Dict, List, Mapping, Sequence, Set, Tuple, Union
|
||||
|
||||
|
||||
from ollama._utils import convert_function_to_tool
|
||||
|
||||
|
||||
@@ -118,7 +117,7 @@ def test_function_with_all_types():
|
||||
|
||||
|
||||
def test_function_docstring_parsing():
|
||||
from typing import List, Dict, Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
def func_with_complex_docs(x: int, y: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user