From 92dc24a0cf880169c2d83ee5184eb76bc391bf87 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 15 Sep 2026 16:37:07 -0700 Subject: [PATCH 1/2] types: warn and drop options the server no longer supports Detect obsolete options and warn if they are detected, then strip them from the request. Unknown options still pass through. Adds min_p and draft_num_predict. Fixes ollama/ollama#18480 --- ollama/_types.py | 74 +++++++++++++++++++++++++++++++++---------- tests/test_options.py | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 tests/test_options.py diff --git a/ollama/_types.py b/ollama/_types.py index b9bb53cb..58823b0a 100644 --- a/ollama/_types.py +++ b/ollama/_types.py @@ -1,5 +1,6 @@ import contextlib import json +import warnings from base64 import b64decode, b64encode from datetime import datetime from pathlib import Path @@ -7,10 +8,12 @@ from pydantic import ( BaseModel, + BeforeValidator, ByteSize, ConfigDict, Field, model_serializer, + model_validator, ) from pydantic.json_schema import JsonSchemaValue from typing_extensions import Annotated, Literal @@ -101,21 +104,49 @@ def get(self, key: str, default: Any = None) -> Any: return getattr(self, key) if hasattr(self, key) else default +# Warn on no longer supported options +_UNSUPPORTED_OPTIONS = frozenset( + { + 'embedding_only', + 'f16_kv', + 'logits_all', + 'low_vram', + 'mirostat', + 'mirostat_eta', + 'mirostat_tau', + 'numa', + 'penalize_newline', + 'tfs_z', + 'typical_p', + 'use_mlock', + 'vocab_only', + } +) + + +def _drop_unsupported_options(options: Any) -> Any: + if not isinstance(options, Mapping): + return options + unsupported = [key for key in options if key in _UNSUPPORTED_OPTIONS] + if not unsupported: + return options + for key in unsupported: + warnings.warn(f'option {key!r} is no longer supported and was ignored', FutureWarning, stacklevel=2) + return {key: value for key, value in options.items() if key not in _UNSUPPORTED_OPTIONS} + + class Options(SubscriptableBaseModel): + # Unknown options pass through so new server options work before the client is updated. + model_config = ConfigDict(extra='allow') + # load time options - numa: Optional[bool] = None num_ctx: Optional[int] = None num_batch: Optional[int] = None num_gpu: Optional[int] = None main_gpu: Optional[int] = None - low_vram: Optional[bool] = None - f16_kv: Optional[bool] = None - logits_all: Optional[bool] = None - vocab_only: Optional[bool] = None use_mmap: Optional[bool] = None - use_mlock: Optional[bool] = None - embedding_only: Optional[bool] = None num_thread: Optional[int] = None + draft_num_predict: Optional[int] = None # runtime options num_keep: Optional[int] = None @@ -123,19 +154,28 @@ class Options(SubscriptableBaseModel): num_predict: Optional[int] = None top_k: Optional[int] = None top_p: Optional[float] = None - tfs_z: Optional[float] = None - typical_p: Optional[float] = None + min_p: Optional[float] = None repeat_last_n: Optional[int] = None temperature: Optional[float] = None repeat_penalty: Optional[float] = None presence_penalty: Optional[float] = None frequency_penalty: Optional[float] = None - mirostat: Optional[int] = None - mirostat_tau: Optional[float] = None - mirostat_eta: Optional[float] = None - penalize_newline: Optional[bool] = None stop: Optional[Sequence[str]] = None + @model_validator(mode='before') + @classmethod + def drop_unsupported(cls, data: Any) -> Any: + return _drop_unsupported_options(data) + + def __setattr__(self, name: str, value: Any) -> None: + if name in _UNSUPPORTED_OPTIONS: + warnings.warn(f'option {name!r} is no longer supported and was ignored', FutureWarning, stacklevel=2) + return + super().__setattr__(name, value) + + +_RequestOptions = Annotated[Optional[Union[Mapping[str, Any], Options]], BeforeValidator(_drop_unsupported_options)] + class BaseRequest(SubscriptableBaseModel): model: Annotated[str, Field(min_length=1)] @@ -148,7 +188,7 @@ class BaseStreamableRequest(BaseRequest): class BaseGenerateRequest(BaseStreamableRequest): - options: Optional[Union[Mapping[str, Any], Options]] = None + options: _RequestOptions = None 'Options to use for the request.' format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None @@ -429,7 +469,7 @@ class EmbedRequest(BaseRequest): truncate: Optional[bool] = None 'Truncate the input to the maximum token length.' - options: Optional[Union[Mapping[str, Any], Options]] = None + options: _RequestOptions = None 'Options to use for the request.' keep_alive: Optional[Union[float, str]] = None @@ -451,7 +491,7 @@ class EmbeddingsRequest(BaseRequest): prompt: Optional[str] = None 'Prompt to generate embeddings from.' - options: Optional[Union[Mapping[str, Any], Options]] = None + options: _RequestOptions = None 'Options to use for the request.' keep_alive: Optional[Union[float, str]] = None @@ -502,7 +542,7 @@ def serialize_model(self, nxt): template: Optional[str] = None license: Optional[Union[str, List[str]]] = None system: Optional[str] = None - parameters: Optional[Union[Mapping[str, Any], Options]] = None + parameters: _RequestOptions = None messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None diff --git a/tests/test_options.py b/tests/test_options.py new file mode 100644 index 00000000..5c1607b6 --- /dev/null +++ b/tests/test_options.py @@ -0,0 +1,58 @@ +import warnings + +import pytest +from pytest_httpserver import HTTPServer + +from ollama._client import Client +from ollama._types import ChatRequest, CreateRequest, Options + + +def test_options_drop_unsupported_keywords(): + with pytest.warns(FutureWarning, match='typical_p'): + options = Options(typical_p=0.5, temperature=0.1) + assert options.model_dump(exclude_none=True) == {'temperature': 0.1} + + +def test_options_drop_unsupported_item_assignment(): + options = Options(temperature=0.1) + with pytest.warns(FutureWarning, match='mirostat'): + options['mirostat'] = 2 + assert options.model_dump(exclude_none=True) == {'temperature': 0.1} + + +def test_options_keep_unknown_keys(): + with warnings.catch_warnings(): + warnings.simplefilter('error') + options = Options(future_option=1, min_p=0.05) + assert options.model_dump(exclude_none=True) == {'future_option': 1, 'min_p': 0.05} + + +def test_request_drops_unsupported_options_mapping(): + with pytest.warns(FutureWarning, match='typical_p'): + request = ChatRequest(model='dummy', messages=[], options={'typical_p': 0.5, 'num_ctx': 8}) + assert request.model_dump(exclude_none=True)['options'] == {'num_ctx': 8} + + +def test_create_request_drops_unsupported_parameters(): + with pytest.warns(FutureWarning, match='penalize_newline'): + request = CreateRequest(model='dummy', parameters={'penalize_newline': True, 'pi': 3.14}) + assert request.model_dump(exclude_none=True)['parameters'] == {'pi': 3.14} + + +def test_client_chat_drops_unsupported_options(httpserver: HTTPServer): + httpserver.expect_ordered_request( + '/api/chat', + method='POST', + json={ + 'model': 'dummy', + 'messages': [{'role': 'user', 'content': 'Hi'}], + 'tools': [], + 'stream': False, + 'options': {'temperature': 0.0}, + }, + ).respond_with_json({'model': 'dummy', 'message': {'role': 'assistant', 'content': 'Hello'}}) + + client = Client(httpserver.url_for('/')) + with pytest.warns(FutureWarning, match='typical_p'): + response = client.chat('dummy', messages=[{'role': 'user', 'content': 'Hi'}], options={'typical_p': 0.5, 'temperature': 0.0}) + assert response['message']['content'] == 'Hello' From a935e5faba8c08dbd3de73879bb190b1bfa9447d Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 15 Sep 2026 16:49:48 -0700 Subject: [PATCH 2/2] ruff: exclude Markdown from formatting ruff 0.16 started formatting Python code blocks in Markdown, so the unpinned `hatch fmt --check` in CI now fails on the untouched README. Keep the README examples hand-wrapped. --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c58f323c..3b32298e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,9 @@ dependencies = [ 'ruff>=0.9.1' ] config-path = 'none' [tool.ruff] -line-length = 320 +line-length = 320 indent-width = 2 +extend-exclude = ['*.md'] [tool.ruff.format] quote-style = 'single'