Spaces:
Runtime error
Runtime error
Commit ·
6de0428
1
Parent(s): 78a6863
fix(llm): local Ollama provider falls back to no-think when model lacks thinking support
Browse files
proteus/providers/ollama_local.py
CHANGED
|
@@ -19,6 +19,7 @@ import os
|
|
| 19 |
|
| 20 |
import httpx
|
| 21 |
|
|
|
|
| 22 |
from proteus.providers.ollama_cloud import OllamaCloudProvider
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
|
@@ -87,3 +88,31 @@ class OllamaLocalProvider(OllamaCloudProvider):
|
|
| 87 |
"OllamaLocalProvider targeting %s at %s (thinking=%s, effort=%s)",
|
| 88 |
model, self._base_url, enable_thinking, reasoning_effort,
|
| 89 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
import httpx
|
| 21 |
|
| 22 |
+
from proteus.providers.base import CompletionResult
|
| 23 |
from proteus.providers.ollama_cloud import OllamaCloudProvider
|
| 24 |
|
| 25 |
logger = logging.getLogger(__name__)
|
|
|
|
| 88 |
"OllamaLocalProvider targeting %s at %s (thinking=%s, effort=%s)",
|
| 89 |
model, self._base_url, enable_thinking, reasoning_effort,
|
| 90 |
)
|
| 91 |
+
|
| 92 |
+
def complete(
|
| 93 |
+
self,
|
| 94 |
+
messages: list[dict[str, str]],
|
| 95 |
+
temperature: float = 0.7,
|
| 96 |
+
max_tokens: int = 4096,
|
| 97 |
+
) -> CompletionResult:
|
| 98 |
+
"""Same native ``/api/chat`` call as the cloud parent, but tolerant of
|
| 99 |
+
models that don't support the ``think`` field.
|
| 100 |
+
|
| 101 |
+
``reasoning_effort`` defaults to ``"low"`` for gpt-oss; a non-reasoning
|
| 102 |
+
local model (e.g. plain qwen/llama) answers Ollama's ``think`` with a
|
| 103 |
+
``400 ... does not support thinking``. On that specific error we disable
|
| 104 |
+
``think`` for the rest of this provider's life and retry once, so the
|
| 105 |
+
same toggle works across reasoning and non-reasoning local models.
|
| 106 |
+
"""
|
| 107 |
+
try:
|
| 108 |
+
return super().complete(messages, temperature, max_tokens)
|
| 109 |
+
except httpx.HTTPStatusError as exc:
|
| 110 |
+
resp = exc.response
|
| 111 |
+
sent_think = self._reasoning_effort is not None or self._enable_thinking is not None
|
| 112 |
+
if (resp is not None and resp.status_code == 400 and sent_think
|
| 113 |
+
and "thinking" in (resp.text or "").lower()):
|
| 114 |
+
logger.info("model %r does not support thinking; disabling and retrying", self._model)
|
| 115 |
+
self._reasoning_effort = None
|
| 116 |
+
self._enable_thinking = None
|
| 117 |
+
return super().complete(messages, temperature, max_tokens)
|
| 118 |
+
raise
|
tests/providers/test_ollama_local.py
CHANGED
|
@@ -50,6 +50,31 @@ def test_complete_sends_native_payload_no_auth_and_parses_thinking():
|
|
| 50 |
assert res.input_tokens == 11 and res.output_tokens == 7
|
| 51 |
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
def test_vanilla_agent_plays_a_move_via_local_provider():
|
| 54 |
# End-to-end: the LLM-play agent parses a valid action from a gpt-oss-style reply.
|
| 55 |
from proteus.game.agents.vanilla import VanillaAgent
|
|
|
|
| 50 |
assert res.input_tokens == 11 and res.output_tokens == 7
|
| 51 |
|
| 52 |
|
| 53 |
+
def test_think_fallback_when_model_does_not_support_thinking():
|
| 54 |
+
# gpt-oss accepts think="low"; a plain (non-reasoning) local model returns a
|
| 55 |
+
# 400 "does not support thinking". The provider must drop think and retry.
|
| 56 |
+
calls = []
|
| 57 |
+
|
| 58 |
+
def handler(request: httpx.Request) -> httpx.Response:
|
| 59 |
+
import json
|
| 60 |
+
body = json.loads(request.content)
|
| 61 |
+
calls.append(body)
|
| 62 |
+
if "think" in body:
|
| 63 |
+
return httpx.Response(400, json={"error": '"qwen" does not support thinking'})
|
| 64 |
+
return httpx.Response(200, json={
|
| 65 |
+
"message": {"content": "ACTION: stay"}, "prompt_eval_count": 3, "eval_count": 2,
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
p = _provider_with_mock(handler, model="qwen2.5:0.5b") # reasoning_effort defaults to "low"
|
| 69 |
+
res = p.complete([{"role": "user", "content": "move"}])
|
| 70 |
+
assert "ACTION: stay" in res.text
|
| 71 |
+
assert len(calls) == 2 # first with think (400), retry without
|
| 72 |
+
assert "think" in calls[0] and "think" not in calls[1]
|
| 73 |
+
# think stays disabled for subsequent calls (no repeated 400 round-trip)
|
| 74 |
+
p.complete([{"role": "user", "content": "again"}])
|
| 75 |
+
assert "think" not in calls[2]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
def test_vanilla_agent_plays_a_move_via_local_provider():
|
| 79 |
# End-to-end: the LLM-play agent parses a valid action from a gpt-oss-style reply.
|
| 80 |
from proteus.game.agents.vanilla import VanillaAgent
|