Skip to content

Commit 2fe220b

Browse files
authored
refactor: split ollama vendor into ollama_local + ollama_cloud (#483)
* refactor(vendor): split VendorType.OLLAMA into OLLAMA_LOCAL + OLLAMA_CLOUD Replace single OLLAMA enum member with OLLAMA_LOCAL ("ollama_local") and OLLAMA_CLOUD ("ollama_cloud"). Add OllamaCloudDefaults with base URL https://ollama.com. Replace OllamaConfig with OllamaLocalConfig (no key required) and OllamaCloudConfig (requires OLLAMA_API_KEY). Update VendorConfig union and __all__ exports. * refactor(validation): resolution service + validators for both ollama slugs Update _VENDOR_CONFIG_MAP to map ollama_local and ollama_cloud to their respective config classes. Fix pre-existing bug: add both ollama slugs to _VALID_BATCH_VENDORS (ollama was previously absent entirely). * refactor(ollama): shared kernel + OllamaLocalClient + OllamaCloudClient Replace OllamaClient with shared module-level helpers parameterized by cloud bool and vendor_slug string, plus two thin client classes: - OllamaLocalClient: overrides invoke() to skip get_api_key, passes format=schema to client.chat() for API-enforced structured output - OllamaCloudClient: uses default BaseClient.invoke() (requires key), omits format param (schema injected into prompt by MessageBuilder) Shared kernel has one forward-compatible seam: the 'if not cloud' guard on the format kwarg. When Ollama Cloud adds structured output support (ollama/ollama#12362), remove this guard. Also adds vendor_slug param to maybe_inject_online_failure for accurate error context. * refactor(invocation): CLIENT_REGISTRY entries for both ollama slugs Register ollama_local and ollama_cloud in CLIENT_REGISTRY with lazy import strings pointing to OllamaLocalClient and OllamaCloudClient. Both map to the 'ollama' pip package in _VENDOR_PACKAGES. * refactor(ollama): batch client vendor_slug + cloud params Parameterize OllamaBatchClient with vendor_slug and cloud flag. Cloud branch validates API key at construction and attaches Bearer header. Format param only passed to client.chat() when not cloud (structured outputs not supported on Ollama Cloud — ollama/ollama#12362). All hard-coded "ollama" strings replaced with self.vendor_slug for JSONL labels, MessageBuilder calls, and error context. * refactor(batch): factory dual registration ollama_local + ollama_cloud Replace _create_ollama with _create_ollama_local (base_url only) and _create_ollama_cloud (api_key + base_url via SecretStr pattern). Register both in _BATCH_CLIENT_REGISTRY. * fix(batch): resolver base_url pass-through + cache-key policy Pass base_url from agent_config into client_config so batch factory honors the same host as online invocation (prevents split-brain FM5). Extend cache key to hash both api_key and base_url — different hosts with the same key must not share a cached client. Update supported_clients list to include ollama_local and ollama_cloud. * refactor(pipeline): message + response + schema for both ollama slugs Register ollama_local and ollama_cloud in PROVIDER_MESSAGE_CONFIGS, PROVIDER_RESPONSE_CONFIGS, and compile_unified_schema. Key design: ollama_cloud uses SchemaInjection.PROMPT (schema injected into prompt text) because Ollama Cloud does not support structured outputs via format param (ollama/ollama#12362). ollama_local keeps SchemaInjection.NONE (API-enforced). Both share UsageShape.OLLAMA. Update _serialise_context to handle both slugs. * refactor(meta): metadata aliases + scanners + context_data Update PROVIDER_ALIASES for both ollama slugs. Update component_scanners config_map (OLLAMA_LOCAL → OllamaLocalConfig, OLLAMA_CLOUD → OllamaCloudConfig). Update context_data vendor list string. * fix(prompt): add SchemaInjection.PROMPT enum member Add missing PROMPT variant to SchemaInjection enum for providers without native structured output support (e.g., Ollama Cloud). Required by ollama_cloud's ProviderMessageConfig added in prior commit. * test: migrate ollama fixtures + mock targets Update 10 test files: replace OllamaClient with OllamaLocalClient, retarget mock patches from _get_client to _build_ollama_client, replace vendor string "ollama" with "ollama_local" in all fixtures, assertions, and parametrize lists. UsageShape.OLLAMA intentionally kept — it names the extraction shape, not a vendor string. 6337 passed, 2 skipped, 0 failures. * test: behavioral tests for local/cloud split 22 tests across 3 classes verifying the behavioral contract between OllamaLocalClient and OllamaCloudClient: - Local: invoke skips get_api_key, no auth header, passes format param - Cloud: invoke requires key, Bearer header, omits format param - Batch: cloud validates key at construction, vendor_slug in labels Every assertion can fail on wrong input. No tautologies. * docs: examples + changie breaking change entry Update all example YAMLs, READMEs, and docs referencing model_vendor: ollama to use ollama_local. Update config_schema description, schema docstring, frontend DAG transformer vendor detection. Add Breaking Change changie entry: model_vendor: ollama removed, use ollama_local or ollama_cloud. * fix: address review findings — manifest, test gaps, minor fixes - Update llm/providers/_MANIFEST.md to describe both ollama_local and ollama_cloud with env vars and structured output limitation - Add ollama_cloud to schema dispatch audit and prompt caching test loops - Add 3 batch base_url tests (local config, cloud default, cloud env) - Fix failure_injection default vendor_slug from "ollama" to "ollama_local" - Fix batch error type from hard-coded "ollama_error" to vendor_slug-based 6362 passed, 2 skipped, 0 failures. * simplify: deduplicate schema extraction, use module-level error mappings - Remove duplicated _extract_ollama_schema from batch_client.py, import the canonical version from client.py (fixes silent fallback bug where batch silently accepted malformed schemas instead of raising) - Replace _error_mapping() function with two module-level constants (_ERROR_MAPPING_LOCAL, _ERROR_MAPPING_CLOUD) matching codebase convention - Replace hardcoded supported-clients list in resolver with BatchClientFactory.get_supported_clients() to prevent stale lists * fix(prompt): implement SchemaInjection.PROMPT for cloud structured output SchemaInjection.PROMPT was declared on ollama_cloud's config but _assemble_body returned "" for PromptStyle.RAW before any injection branch could fire, and had no PROMPT branch at all. Cloud JSON calls received no schema signal — models returned unstructured text, causing VendorAPIError on parse. Fix: inject schema into prompt_config before _wrap_in_roles, so it lands in the system message. Cloud models now see the schema as a JSON instruction in the prompt text. Add two tests: - test_cloud_schema_injected_into_prompt (positive: schema in system msg) - test_local_schema_not_in_prompt (negative: local uses format param) * fix: skip schema extraction for cloud, move json import to module scope - Short-circuit _extract_ollama_schema for cloud (avoids wasted work and false ConfigurationError on malformed schemas cloud never sends) - Move json import to module scope in message_builder.py, remove two redundant function-scope imports
1 parent 04ecb0f commit 2fe220b

44 files changed

Lines changed: 1109 additions & 454 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Breaking Change
2+
body: "model_vendor: ollama removed. Use ollama_local (local daemon, no auth) or ollama_cloud (ollama.com, requires OLLAMA_API_KEY)."
3+
time: 2026-05-03T06:00:00.000000Z

agent_actions/config/defaults.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ class OllamaDefaults:
2020
BASE_URL: str = "http://localhost:11434"
2121

2222

23+
class OllamaCloudDefaults:
24+
"""Defaults for Ollama Cloud provider (ollama.com)."""
25+
26+
BASE_URL: str = "https://ollama.com"
27+
28+
2329
class ApiDefaults:
2430
"""Defaults for HTTP/API data-source fetching."""
2531

agent_actions/llm/batch/infrastructure/batch_client_resolver.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def get_for_config(self, agent_config: dict[str, Any]) -> BaseBatchClient:
7474
"'tool' vendor does not support batch processing",
7575
context={
7676
"client_type": client_type,
77-
"supported_clients": ["openai", "gemini", "anthropic", "groq", "mistral"],
77+
"supported_clients": BatchClientFactory.get_supported_clients(),
7878
},
7979
)
8080

@@ -91,9 +91,11 @@ def get_for_config(self, agent_config: dict[str, Any]) -> BaseBatchClient:
9191
)
9292

9393
try:
94-
client_config = {}
94+
client_config: dict[str, Any] = {}
9595
if agent_config.get(API_KEY_KEY):
9696
client_config["api_key"] = BaseClient.get_api_key(agent_config)
97+
if agent_config.get("base_url"):
98+
client_config["base_url"] = agent_config["base_url"]
9799

98100
client = BatchClientFactory.create_client(client_type, client_config)
99101

@@ -180,8 +182,10 @@ def _find_cached_client(self, client_type: str) -> BaseBatchClient | None:
180182
def _build_cache_key(client_type: str, agent_config: dict[str, Any]) -> str:
181183
_raw = agent_config.get(API_KEY_KEY) or ""
182184
api_key = _raw.get_secret_value() if isinstance(_raw, SecretStr) else _raw
183-
if api_key:
184-
key_hash = hashlib.sha256(api_key.encode()).hexdigest()[:12]
185+
base_url = agent_config.get("base_url") or ""
186+
discriminator = f"{api_key}|{base_url}"
187+
if api_key or base_url:
188+
key_hash = hashlib.sha256(discriminator.encode()).hexdigest()[:12]
185189
return f"{client_type}:{key_hash}"
186190
return client_type
187191

agent_actions/llm/config/vendor.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from pydantic import BaseModel, Field
99

10-
from agent_actions.config.defaults import OllamaDefaults
10+
from agent_actions.config.defaults import OllamaCloudDefaults, OllamaDefaults
1111

1212

1313
class VendorType(str, Enum):
@@ -22,7 +22,8 @@ class VendorType(str, Enum):
2222
GROQ = "groq"
2323
COHERE = "cohere"
2424
MISTRAL = "mistral"
25-
OLLAMA = "ollama"
25+
OLLAMA_LOCAL = "ollama_local"
26+
OLLAMA_CLOUD = "ollama_cloud"
2627
TOOL = "tool"
2728
HITL = "hitl"
2829
AGAC_PROVIDER = "agac-provider"
@@ -110,12 +111,28 @@ class MistralConfig(BaseVendorConfig):
110111
api_key_env_name: str = "MISTRAL_API_KEY"
111112

112113

113-
class OllamaConfig(BaseVendorConfig):
114-
"""Configuration specific to Ollama (local models)."""
114+
class OllamaLocalConfig(BaseVendorConfig):
115+
"""Configuration specific to Ollama local daemon.
115116
116-
vendor_type: Literal[VendorType.OLLAMA] = VendorType.OLLAMA
117+
No API key required — the local daemon does not use Bearer auth.
118+
"""
119+
120+
vendor_type: Literal[VendorType.OLLAMA_LOCAL] = VendorType.OLLAMA_LOCAL
121+
api_key_env_name: str = "NO_KEY_REQUIRED"
122+
base_url: str = Field(default=OllamaDefaults.BASE_URL, description="Ollama local server URL")
123+
124+
125+
class OllamaCloudConfig(BaseVendorConfig):
126+
"""Configuration specific to Ollama Cloud (ollama.com).
127+
128+
Requires OLLAMA_API_KEY for Bearer authentication.
129+
"""
130+
131+
vendor_type: Literal[VendorType.OLLAMA_CLOUD] = VendorType.OLLAMA_CLOUD
117132
api_key_env_name: str = "OLLAMA_API_KEY"
118-
base_url: str = Field(default=OllamaDefaults.BASE_URL, description="Ollama server URL")
133+
base_url: str = Field(
134+
default=OllamaCloudDefaults.BASE_URL, description="Ollama Cloud server URL"
135+
)
119136

120137

121138
class ToolVendorConfig(BaseVendorConfig):
@@ -159,7 +176,8 @@ class AgacProviderConfig(BaseVendorConfig):
159176
| GroqConfig
160177
| CohereConfig
161178
| MistralConfig
162-
| OllamaConfig
179+
| OllamaLocalConfig
180+
| OllamaCloudConfig
163181
| ToolVendorConfig
164182
| HitlVendorConfig
165183
| AgacProviderConfig
@@ -214,7 +232,8 @@ def create_default_registry(cls) -> "VendorRegistry":
214232
"GroqConfig",
215233
"CohereConfig",
216234
"MistralConfig",
217-
"OllamaConfig",
235+
"OllamaLocalConfig",
236+
"OllamaCloudConfig",
218237
"ToolVendorConfig",
219238
"HitlVendorConfig",
220239
"AgacProviderConfig",

agent_actions/llm/providers/_MANIFEST.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ shared utilities (mixins, usage tracking) for each supported LLM vendor.
2323
| `gemini/__init__.py` | Module | Gemini provider wiring (Google Gemini). | `llm.providers`, `google` |
2424
| `groq/__init__.py` | Module | Groq provider binding for Groq Cloud inference. | `llm.providers`, `groq` |
2525
| `mistral/__init__.py` | Module | Mistral provider integration. | `llm.providers`, `mistral` |
26-
| `ollama/__init__.py` | Module | Ollama provider connector for local inference. | `llm.providers`, `ollama` |
26+
| `ollama/__init__.py` | Module | Ollama provider with dual registration: `ollama_local` (no auth, `OLLAMA_HOST`, API-enforced structured output via `format` param) and `ollama_cloud` (Bearer auth via `OLLAMA_API_KEY`, `OLLAMA_CLOUD_HOST`, no structured output support yet). | `llm.providers`, `ollama` |
2727
| `openai/__init__.py` | Module | OpenAI provider adapter with tool/window shaping. | `llm.providers`, `openai` |
2828
| `tools/__init__.py` | Module | Tool discovery/registration for OpenAI function calling formats. | `llm.providers`, `tools` |
2929
| `hitl/__init__.py` | Module | Human-in-the-loop provider for synchronous approval/rejection workflows. | `llm.providers`, `hitl` |

agent_actions/llm/providers/batch_client_factory.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from pydantic import SecretStr
1515

16-
from agent_actions.config.defaults import OllamaDefaults
16+
from agent_actions.config.defaults import OllamaCloudDefaults, OllamaDefaults
1717

1818
from .batch_base import BaseBatchClient
1919

@@ -86,11 +86,26 @@ def _create_gemini(config: dict[str, Any]) -> BaseBatchClient:
8686
return cls(api_key=api_key) # type: ignore[no-any-return]
8787

8888

89-
def _create_ollama(config: dict[str, Any]) -> BaseBatchClient:
89+
def _create_ollama_local(config: dict[str, Any]) -> BaseBatchClient:
9090
from .ollama.batch_client import OllamaBatchClient
9191

9292
base_url = config.get("base_url") or os.getenv("OLLAMA_HOST", OllamaDefaults.BASE_URL)
93-
return OllamaBatchClient(base_url=base_url)
93+
return OllamaBatchClient(base_url=base_url, vendor_slug="ollama_local", cloud=False)
94+
95+
96+
def _create_ollama_cloud(config: dict[str, Any]) -> BaseBatchClient:
97+
from .ollama.batch_client import OllamaBatchClient
98+
99+
_raw = config.get("api_key")
100+
api_key = (_raw.get_secret_value() if isinstance(_raw, SecretStr) else _raw) or os.getenv(
101+
"OLLAMA_API_KEY"
102+
)
103+
base_url = config.get("base_url") or os.getenv(
104+
"OLLAMA_CLOUD_HOST", OllamaCloudDefaults.BASE_URL
105+
)
106+
return OllamaBatchClient(
107+
base_url=base_url, api_key=api_key, vendor_slug="ollama_cloud", cloud=True
108+
)
94109

95110

96111
def _create_anthropic(config: dict[str, Any]) -> BaseBatchClient:
@@ -139,7 +154,8 @@ def _create_agac(config: dict[str, Any]) -> BaseBatchClient:
139154
_BATCH_CLIENT_REGISTRY: dict[str, _BatchClientRegistration] = {
140155
"openai": _BatchClientRegistration(factory=_create_openai),
141156
"gemini": _BatchClientRegistration(factory=_create_gemini, package="google-genai"),
142-
"ollama": _BatchClientRegistration(factory=_create_ollama),
157+
"ollama_local": _BatchClientRegistration(factory=_create_ollama_local),
158+
"ollama_cloud": _BatchClientRegistration(factory=_create_ollama_cloud),
143159
"anthropic": _BatchClientRegistration(factory=_create_anthropic, package="anthropic"),
144160
"groq": _BatchClientRegistration(factory=_create_groq, package="groq"),
145161
"mistral": _BatchClientRegistration(factory=_create_mistral, package="mistralai"),

agent_actions/llm/providers/ollama/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
# pyright: reportImportCycles=false
33

44
from agent_actions.llm.providers.ollama.batch_client import OllamaBatchClient
5-
from agent_actions.llm.providers.ollama.client import OllamaClient
5+
from agent_actions.llm.providers.ollama.client import OllamaCloudClient, OllamaLocalClient
66

77
__all__ = [
8-
"OllamaClient",
8+
"OllamaLocalClient",
9+
"OllamaCloudClient",
910
"OllamaBatchClient",
1011
]

0 commit comments

Comments
 (0)