Skip to content

Commit b12e543

Browse files
Merge pull request #268 from askui/feat/ollama_provider
feat: add OpenAI-compatible and Ollama model providers
2 parents 12b46f6 + 00aaaa1 commit b12e543

18 files changed

Lines changed: 1738 additions & 0 deletions

src/askui/model_providers/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
- `AnthropicVlmProvider` — VLM via direct Anthropic API
1313
- `AnthropicImageQAProvider` — image Q&A via direct Anthropic API
1414
- `GoogleImageQAProvider` — image Q&A via Google Gemini API (direct, no proxy)
15+
- `OpenAIVlmProvider` — VLM via any OpenAI-compatible API
16+
- `OpenAIImageQAProvider` — image Q&A via any OpenAI-compatible API
17+
- `OllamaVlmProvider` — VLM via local Ollama instance (OpenAI-compatible)
18+
- `OllamaImageQAProvider` — image Q&A via local Ollama instance (OpenAI-compatible)
19+
- `OpenAICompatibleVlmProvider` — VLM via OpenAI-compatible API with fixed URL
1520
"""
1621

1722
from askui.model_providers.anthropic_image_qa_provider import AnthropicImageQAProvider
@@ -22,6 +27,13 @@
2227
from askui.model_providers.detection_provider import DetectionProvider
2328
from askui.model_providers.google_image_qa_provider import GoogleImageQAProvider
2429
from askui.model_providers.image_qa_provider import ImageQAProvider
30+
from askui.model_providers.ollama_image_qa_provider import OllamaImageQAProvider
31+
from askui.model_providers.ollama_vlm_provider import OllamaVlmProvider
32+
from askui.model_providers.openai_compatible_vlm_provider import (
33+
OpenAICompatibleVlmProvider,
34+
)
35+
from askui.model_providers.openai_image_qa_provider import OpenAIImageQAProvider
36+
from askui.model_providers.openai_vlm_provider import OpenAIVlmProvider
2537
from askui.model_providers.vlm_provider import VlmProvider
2638
from askui.utils.model_pricing import ModelPricing
2739

@@ -35,5 +47,10 @@
3547
"GoogleImageQAProvider",
3648
"ImageQAProvider",
3749
"ModelPricing",
50+
"OllamaImageQAProvider",
51+
"OllamaVlmProvider",
52+
"OpenAIImageQAProvider",
53+
"OpenAIVlmProvider",
54+
"OpenAICompatibleVlmProvider",
3855
"VlmProvider",
3956
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""OllamaImageQAProvider — image Q&A via a local Ollama instance."""
2+
3+
from openai import OpenAI
4+
5+
from askui.model_providers.openai_image_qa_provider import OpenAIImageQAProvider
6+
7+
_DEFAULT_BASE_URL = "http://localhost:11434/v1"
8+
_DEFAULT_MODEL_ID = "qwen3.5"
9+
10+
11+
class OllamaImageQAProvider(OpenAIImageQAProvider):
12+
"""Image Q&A provider that routes requests to a local Ollama instance.
13+
14+
Thin convenience wrapper around `OpenAIImageQAProvider` with Ollama
15+
defaults (``base_url``, ``api_key``, ``model_id``).
16+
17+
Args:
18+
model_id (str, optional): Ollama model to use. Defaults to
19+
``"qwen3.5"``.
20+
base_url (str, optional): Base URL for the Ollama OpenAI-compatible
21+
API. Defaults to ``"http://localhost:11434/v1"``.
22+
client (`OpenAI` | None, optional): Pre-configured OpenAI client.
23+
If provided, ``base_url`` is ignored.
24+
25+
Example:
26+
```python
27+
from askui import AgentSettings, ComputerAgent
28+
from askui.model_providers import OllamaImageQAProvider
29+
30+
agent = ComputerAgent(settings=AgentSettings(
31+
image_qa_provider=OllamaImageQAProvider(
32+
model_id="llava",
33+
)
34+
))
35+
```
36+
"""
37+
38+
def __init__(
39+
self,
40+
model_id: str = _DEFAULT_MODEL_ID,
41+
base_url: str = _DEFAULT_BASE_URL,
42+
client: OpenAI | None = None,
43+
) -> None:
44+
super().__init__(
45+
model_id=model_id,
46+
api_key="ollama", # Ollama requires no auth; OpenAI SDK needs a value
47+
base_url=base_url,
48+
client=client,
49+
)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""OllamaVlmProvider — VLM access via a local Ollama instance."""
2+
3+
from openai import OpenAI
4+
5+
from askui.model_providers.openai_vlm_provider import OpenAIVlmProvider
6+
7+
_DEFAULT_BASE_URL = "http://localhost:11434/v1"
8+
_DEFAULT_MODEL_ID = "qwen3.5"
9+
10+
11+
class OllamaVlmProvider(OpenAIVlmProvider):
12+
"""VLM provider that routes requests to a local Ollama instance.
13+
14+
Thin convenience wrapper around `OpenAIVlmProvider` with Ollama
15+
defaults (``base_url``, ``api_key``, ``model_id``).
16+
17+
Args:
18+
model_id (str, optional): Ollama model to use. Defaults to
19+
``"qwen3.5"``.
20+
base_url (str, optional): Base URL for the Ollama OpenAI-compatible
21+
API. Defaults to ``"http://localhost:11434/v1"``.
22+
client (`OpenAI` | None, optional): Pre-configured OpenAI client.
23+
If provided, ``base_url`` is ignored.
24+
25+
Example:
26+
```python
27+
from askui import AgentSettings, ComputerAgent
28+
from askui.model_providers import OllamaVlmProvider
29+
30+
agent = ComputerAgent(settings=AgentSettings(
31+
vlm_provider=OllamaVlmProvider(
32+
model_id="qwen3.5",
33+
)
34+
))
35+
```
36+
"""
37+
38+
def __init__(
39+
self,
40+
model_id: str = _DEFAULT_MODEL_ID,
41+
base_url: str = _DEFAULT_BASE_URL,
42+
client: OpenAI | None = None,
43+
) -> None:
44+
super().__init__(
45+
model_id=model_id,
46+
api_key="ollama", # Ollama requires no auth; OpenAI SDK needs a value
47+
base_url=base_url,
48+
client=client,
49+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""OpenAICompatibleVlmProvider — VLM access via a fixed endpoint URL."""
2+
3+
import httpx
4+
from openai import OpenAI
5+
6+
from askui.model_providers.openai_vlm_provider import OpenAIVlmProvider
7+
8+
9+
class OpenAICompatibleVlmProvider(OpenAIVlmProvider):
10+
"""VLM provider for OpenAI-compatible APIs that require an exact endpoint URL.
11+
12+
The OpenAI SDK always appends ``/chat/completions`` to ``base_url``,
13+
which breaks endpoints that already include the full path (e.g. RunPod,
14+
custom proxies, serverless deployments). This provider works around
15+
the issue by installing an httpx event hook that rewrites every
16+
outgoing request URL to the exact ``endpoint_url``.
17+
18+
Args:
19+
endpoint_url (str): Full endpoint URL including the path
20+
(e.g. ``"https://my-host/v1/chat/completions"``).
21+
model_id (str): Model name expected by the deployment.
22+
api_key (str | None, optional): API key for the endpoint.
23+
24+
Example:
25+
```python
26+
from askui import AgentSettings, ComputerAgent
27+
from askui.model_providers import OpenAICompatibleVlmProvider
28+
29+
agent = ComputerAgent(settings=AgentSettings(
30+
vlm_provider=OpenAICompatibleVlmProvider(
31+
endpoint_url="https://my-host/v1/chat/completions",
32+
model_id="my-model",
33+
api_key="...",
34+
)
35+
))
36+
```
37+
"""
38+
39+
def __init__(
40+
self,
41+
endpoint_url: str,
42+
model_id: str | None = None,
43+
api_key: str | None = None,
44+
) -> None:
45+
def _rewrite_url(request: httpx.Request) -> None:
46+
request.url = httpx.URL(endpoint_url)
47+
48+
http_client = httpx.Client(event_hooks={"request": [_rewrite_url]})
49+
50+
client = OpenAI(
51+
api_key=api_key,
52+
base_url=endpoint_url,
53+
http_client=http_client,
54+
)
55+
56+
super().__init__(
57+
model_id=model_id,
58+
client=client,
59+
)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""OpenAIImageQAProvider — image Q&A via any OpenAI-compatible API."""
2+
3+
from functools import cached_property
4+
5+
from openai import OpenAI
6+
from typing_extensions import override
7+
8+
from askui.model_providers.image_qa_provider import ImageQAProvider
9+
from askui.models.openai.get_model import OpenAIGetModel
10+
from askui.models.shared.settings import GetSettings
11+
from askui.models.types.response_schemas import ResponseSchema
12+
from askui.utils.source_utils import Source
13+
14+
15+
class OpenAIImageQAProvider(ImageQAProvider):
16+
"""Image Q&A provider for any OpenAI-compatible API.
17+
18+
Works with OpenAI, Ollama, vLLM, LM Studio, Together AI, and any
19+
other service that exposes an OpenAI-compatible ``/v1/chat/completions``
20+
endpoint.
21+
22+
Args:
23+
model_id (str): Model name to use.
24+
api_key (str | None, optional): API key. Reads ``OPENAI_API_KEY``
25+
from the environment if not provided.
26+
base_url (str | None, optional): Base URL for the API. Defaults
27+
to the OpenAI API (``https://api.openai.com/v1``).
28+
client (`OpenAI` | None, optional): Pre-configured OpenAI client.
29+
If provided, ``api_key`` and ``base_url`` are ignored.
30+
31+
Example:
32+
```python
33+
from askui import AgentSettings, ComputerAgent
34+
from askui.model_providers import OpenAIImageQAProvider
35+
36+
agent = ComputerAgent(settings=AgentSettings(
37+
image_qa_provider=OpenAIImageQAProvider(
38+
model_id="gpt-4o",
39+
api_key="sk-...",
40+
)
41+
))
42+
```
43+
"""
44+
45+
def __init__(
46+
self,
47+
model_id: str,
48+
api_key: str | None = None,
49+
base_url: str | None = None,
50+
client: OpenAI | None = None,
51+
) -> None:
52+
self._model_id = model_id
53+
self._client = client or OpenAI(
54+
api_key=api_key,
55+
base_url=base_url,
56+
)
57+
58+
@cached_property
59+
def _get_model(self) -> OpenAIGetModel:
60+
"""Lazily initialise the `OpenAIGetModel` on first use."""
61+
return OpenAIGetModel(model_id=self._model_id, client=self._client)
62+
63+
@override
64+
def query(
65+
self,
66+
query: str,
67+
source: Source,
68+
response_schema: type[ResponseSchema] | None,
69+
get_settings: GetSettings,
70+
) -> ResponseSchema | str:
71+
result: ResponseSchema | str = self._get_model.get(
72+
query=query,
73+
source=source,
74+
response_schema=response_schema,
75+
get_settings=get_settings,
76+
)
77+
return result
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""OpenAIVlmProvider — VLM access via any OpenAI-compatible API."""
2+
3+
import os
4+
from functools import cached_property
5+
from typing import Any
6+
7+
from openai import OpenAI
8+
from typing_extensions import override
9+
10+
from askui.model_providers.vlm_provider import VlmProvider
11+
from askui.models.openai.messages_api import OpenAIMessagesApi
12+
from askui.models.shared.agent_message_param import (
13+
MessageParam,
14+
ThinkingConfigParam,
15+
ToolChoiceParam,
16+
)
17+
from askui.models.shared.prompts import SystemPrompt
18+
from askui.models.shared.tools import ToolCollection
19+
from askui.utils.model_pricing import ModelPricing
20+
21+
_DEFAULT_MODEL_ID = "gpt-5.4"
22+
23+
24+
class OpenAIVlmProvider(VlmProvider):
25+
"""VLM provider for any OpenAI-compatible API.
26+
27+
Works with OpenAI, Ollama, vLLM, LM Studio, Together AI, and any
28+
other service that exposes an OpenAI-compatible ``/v1/chat/completions``
29+
endpoint.
30+
31+
Args:
32+
model_id (str): Model name to use.
33+
api_key (str | None, optional): API key. Reads ``OPENAI_API_KEY``
34+
from the environment if not provided.
35+
base_url (str | None, optional): Base URL for the API. Defaults
36+
to the OpenAI API (``https://api.openai.com/v1``).
37+
client (`OpenAI` | None, optional): Pre-configured OpenAI client.
38+
If provided, ``api_key`` and ``base_url`` are ignored.
39+
40+
Example:
41+
```python
42+
from askui import AgentSettings, ComputerAgent
43+
from askui.model_providers import OpenAIVlmProvider
44+
45+
agent = ComputerAgent(settings=AgentSettings(
46+
vlm_provider=OpenAIVlmProvider(
47+
model_id="gpt-4o",
48+
api_key="sk-...",
49+
)
50+
))
51+
```
52+
"""
53+
54+
def __init__(
55+
self,
56+
model_id: str | None = None,
57+
api_key: str | None = None,
58+
base_url: str | None = None,
59+
client: OpenAI | None = None,
60+
input_cost_per_million_tokens: float | None = None,
61+
output_cost_per_million_tokens: float | None = None,
62+
cache_write_cost_per_million_tokens: float | None = None,
63+
cache_read_cost_per_million_tokens: float | None = None,
64+
) -> None:
65+
self._model_id_value = (
66+
model_id or os.environ.get("VLM_PROVIDER_MODEL_ID") or _DEFAULT_MODEL_ID
67+
)
68+
if client is not None:
69+
self._client = client
70+
else:
71+
self._client = OpenAI(
72+
api_key=api_key,
73+
base_url=base_url,
74+
)
75+
76+
self._pricing = ModelPricing.for_model(
77+
self._model_id_value,
78+
input_cost_per_million_tokens=input_cost_per_million_tokens,
79+
output_cost_per_million_tokens=output_cost_per_million_tokens,
80+
cache_write_cost_per_million_tokens=cache_write_cost_per_million_tokens,
81+
cache_read_cost_per_million_tokens=cache_read_cost_per_million_tokens,
82+
)
83+
84+
@property
85+
@override
86+
def model_id(self) -> str:
87+
return self._model_id_value
88+
89+
@property
90+
@override
91+
def pricing(self) -> ModelPricing | None:
92+
return self._pricing
93+
94+
@cached_property
95+
def _messages_api(self) -> OpenAIMessagesApi:
96+
"""Lazily initialise the `OpenAIMessagesApi` on first use."""
97+
return OpenAIMessagesApi(client=self._client)
98+
99+
@override
100+
def create_message(
101+
self,
102+
messages: list[MessageParam],
103+
tools: ToolCollection | None = None,
104+
max_tokens: int | None = None,
105+
system: SystemPrompt | None = None,
106+
thinking: ThinkingConfigParam | None = None,
107+
tool_choice: ToolChoiceParam | None = None,
108+
temperature: float | None = None,
109+
provider_options: dict[str, Any] | None = None,
110+
) -> MessageParam:
111+
return self._messages_api.create_message(
112+
messages=messages,
113+
model_id=self._model_id_value,
114+
tools=tools,
115+
max_tokens=max_tokens,
116+
system=system,
117+
thinking=thinking,
118+
tool_choice=tool_choice,
119+
temperature=temperature,
120+
provider_options=provider_options,
121+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Model integration via OpenAI-compatible APIs."""

0 commit comments

Comments
 (0)