Skip to content

Commit c5491bf

Browse files
committed
Add tests and improve auth header
1 parent 17fb82e commit c5491bf

File tree

3 files changed

+103
-21
lines changed

3 files changed

+103
-21
lines changed

examples/web-search-crawl.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from rich import print
1212

13-
from ollama import Client, WebCrawlResponse, WebSearchResponse
13+
from ollama import WebCrawlResponse, WebSearchResponse, web_search, web_crawl, chat
1414

1515

1616
def format_tool_results(results: Union[WebSearchResponse, WebCrawlResponse]):
@@ -49,15 +49,17 @@ def format_tool_results(results: Union[WebSearchResponse, WebCrawlResponse]):
4949
return '\n'.join(output).rstrip()
5050

5151

52-
client = Client(headers={'Authorization': (os.getenv('OLLAMA_API_KEY'))})
53-
available_tools = {'web_search': client.web_search, 'web_crawl': client.web_crawl}
52+
# Set OLLAMA_API_KEY in the environment variable or use the headers parameter to set the authorization header
53+
# client = Client(headers={'Authorization': 'Bearer <OLLAMA_API_KEY>'})
54+
55+
available_tools = {'web_search': web_search, 'web_crawl': web_crawl}
5456

5557
query = "ollama's new engine"
5658
print('Query: ', query)
5759

5860
messages = [{'role': 'user', 'content': query}]
5961
while True:
60-
response = client.chat(model='qwen3', messages=messages, tools=[client.web_search, client.web_crawl], think=True)
62+
response = chat(model='qwen3', messages=messages, tools=[web_search, web_crawl], think=True)
6163
if response.message.thinking:
6264
print('Thinking: ')
6365
print(response.message.thinking + '\n\n')

ollama/_client.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,25 @@ def __init__(
9494
`kwargs` are passed to the httpx client.
9595
"""
9696

97+
headers = {
98+
k.lower(): v
99+
for k, v in {
100+
**(headers or {}),
101+
'Content-Type': 'application/json',
102+
'Accept': 'application/json',
103+
'User-Agent': f'ollama-python/{__version__} ({platform.machine()} {platform.system().lower()}) Python/{platform.python_version()}',
104+
}.items()
105+
if v is not None
106+
}
97107
api_key = os.getenv('OLLAMA_API_KEY', None)
108+
if not headers.get('authorization') and api_key:
109+
headers['authorization'] = f'Bearer {api_key}'
98110

99111
self._client = client(
100112
base_url=_parse_host(host or os.getenv('OLLAMA_HOST')),
101113
follow_redirects=follow_redirects,
102114
timeout=timeout,
103-
# Lowercase all headers to ensure override
104-
headers={
105-
k.lower(): v
106-
for k, v in {
107-
**(headers or {}),
108-
'Content-Type': 'application/json',
109-
'Accept': 'application/json',
110-
'User-Agent': f'ollama-python/{__version__} ({platform.machine()} {platform.system().lower()}) Python/{platform.python_version()}',
111-
'Authorization': f'Bearer {api_key}' if api_key else '',
112-
}.items()
113-
},
115+
headers=headers,
114116
**kwargs,
115117
)
116118

@@ -644,9 +646,8 @@ def web_search(self, queries: Sequence[str], max_results: int = 3) -> WebSearchR
644646
Raises:
645647
ValueError: If OLLAMA_API_KEY environment variable is not set
646648
"""
647-
api_key = os.getenv('OLLAMA_API_KEY')
648-
if not api_key:
649-
raise ValueError('OLLAMA_API_KEY environment variable is required for web search')
649+
if not self._client.headers.get('authorization', '').startswith('Bearer '):
650+
raise ValueError('Authorization header with Bearer token is required for web search')
650651

651652
return self._request(
652653
WebSearchResponse,
@@ -670,9 +671,8 @@ def web_crawl(self, urls: Sequence[str]) -> WebCrawlResponse:
670671
Raises:
671672
ValueError: If OLLAMA_API_KEY environment variable is not set
672673
"""
673-
api_key = os.getenv('OLLAMA_API_KEY')
674-
if not api_key:
675-
raise ValueError('OLLAMA_API_KEY environment variable is required for web fetch')
674+
if not self._client.headers.get('authorization', '').startswith('Bearer '):
675+
raise ValueError('Authorization header with Bearer token is required for web fetch')
676676

677677
return self._request(
678678
WebCrawlResponse,

tests/test_client.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,3 +1195,83 @@ async def test_arbitrary_roles_accepted_in_message_request_async(monkeypatch: py
11951195
client = AsyncClient()
11961196

11971197
await client.chat(model='llama3.1', messages=[{'role': 'somerandomrole', 'content': "I'm ok with you adding any role message now!"}, {'role': 'user', 'content': 'Hello world!'}])
1198+
1199+
1200+
def test_client_web_search_requires_bearer_auth_header(monkeypatch: pytest.MonkeyPatch):
1201+
monkeypatch.delenv('OLLAMA_API_KEY', raising=False)
1202+
1203+
client = Client()
1204+
1205+
with pytest.raises(ValueError, match='Authorization header with Bearer token is required for web search'):
1206+
client.web_search(['test query'])
1207+
1208+
1209+
def test_client_web_crawl_requires_bearer_auth_header(monkeypatch: pytest.MonkeyPatch):
1210+
monkeypatch.delenv('OLLAMA_API_KEY', raising=False)
1211+
1212+
client = Client()
1213+
1214+
with pytest.raises(ValueError, match='Authorization header with Bearer token is required for web fetch'):
1215+
client.web_crawl(['https://example.com'])
1216+
1217+
1218+
def _mock_request_web_search(self, cls, method, url, json=None, **kwargs):
1219+
assert method == 'POST'
1220+
assert url == 'https://ollama.com/api/web_search'
1221+
assert json is not None and 'queries' in json and 'max_results' in json
1222+
return httpxResponse(status_code=200, content='{"results": {}, "success": true}')
1223+
1224+
1225+
def _mock_request_web_crawl(self, cls, method, url, json=None, **kwargs):
1226+
assert method == 'POST'
1227+
assert url == 'https://ollama.com/api/web_crawl'
1228+
assert json is not None and 'urls' in json
1229+
return httpxResponse(status_code=200, content='{"results": {}, "success": true}')
1230+
1231+
1232+
def test_client_web_search_with_env_api_key(monkeypatch: pytest.MonkeyPatch):
1233+
monkeypatch.setenv('OLLAMA_API_KEY', 'test-key')
1234+
monkeypatch.setattr(Client, '_request', _mock_request_web_search)
1235+
1236+
client = Client()
1237+
client.web_search(['what is ollama?'], max_results=2)
1238+
1239+
1240+
def test_client_web_crawl_with_env_api_key(monkeypatch: pytest.MonkeyPatch):
1241+
monkeypatch.setenv('OLLAMA_API_KEY', 'test-key')
1242+
monkeypatch.setattr(Client, '_request', _mock_request_web_crawl)
1243+
1244+
client = Client()
1245+
client.web_crawl(['https://example.com'])
1246+
1247+
1248+
def test_client_web_search_with_explicit_bearer_header(monkeypatch: pytest.MonkeyPatch):
1249+
monkeypatch.delenv('OLLAMA_API_KEY', raising=False)
1250+
monkeypatch.setattr(Client, '_request', _mock_request_web_search)
1251+
1252+
client = Client(headers={'Authorization': 'Bearer custom-token'})
1253+
client.web_search(['what is ollama?'], max_results=1)
1254+
1255+
1256+
def test_client_web_crawl_with_explicit_bearer_header(monkeypatch: pytest.MonkeyPatch):
1257+
monkeypatch.delenv('OLLAMA_API_KEY', raising=False)
1258+
monkeypatch.setattr(Client, '_request', _mock_request_web_crawl)
1259+
1260+
client = Client(headers={'Authorization': 'Bearer custom-token'})
1261+
client.web_crawl(['https://example.com'])
1262+
1263+
1264+
def test_client_bearer_header_from_env(monkeypatch: pytest.MonkeyPatch):
1265+
monkeypatch.setenv('OLLAMA_API_KEY', 'env-token')
1266+
1267+
client = Client()
1268+
assert client._client.headers['authorization'] == 'Bearer env-token'
1269+
1270+
1271+
def test_client_explicit_bearer_header_overrides_env(monkeypatch: pytest.MonkeyPatch):
1272+
monkeypatch.setenv('OLLAMA_API_KEY', 'env-token')
1273+
monkeypatch.setattr(Client, '_request', _mock_request_web_search)
1274+
1275+
client = Client(headers={'Authorization': 'Bearer explicit-token'})
1276+
assert client._client.headers['authorization'] == 'Bearer explicit-token'
1277+
client.web_search(['override check'])

0 commit comments

Comments
 (0)