Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4298,6 +4298,7 @@ def _retry_same_provider_sync(
reasoning_config=reasoning_config,
base_url=retry_base or resolved_base_url,
task=task,
api_key=resolved_api_key,
)
# Preserve per-request attribution headers (e.g. Copilot's
# ``x-initiator: user``) across the rebuilt-client retry — dropping them
Expand Down Expand Up @@ -4370,6 +4371,7 @@ async def _retry_same_provider_async(
reasoning_config=reasoning_config,
base_url=retry_base or resolved_base_url,
task=task,
api_key=resolved_api_key,
)
# Preserve per-request attribution headers across the rebuilt-client
# retry — see the sync variant above (#60293).
Expand Down Expand Up @@ -4696,7 +4698,10 @@ def _call_fallback_candidate_sync(
temperature=temperature, max_tokens=max_tokens,
tools=fallback_tools, timeout=effective_timeout,
extra_body=effective_extra_body, reasoning_config=reasoning_config,
base_url=destination.base_url, task=task)
base_url=destination.base_url,
task=task,
api_key=getattr(fb_client, "api_key", None),
)
try:
return _validate_llm_response(
_relay_sync_completion(
Expand Down Expand Up @@ -4741,7 +4746,10 @@ def _call_fallback_candidate_sync(
tools=retry_tools, timeout=effective_timeout,
extra_body=effective_extra_body,
reasoning_config=reasoning_config,
base_url=retry_destination.base_url, task=task)
base_url=retry_destination.base_url,
task=task,
api_key=getattr(retry_client, "api_key", None),
)
try:
return _validate_llm_response(
_relay_sync_completion(
Expand Down Expand Up @@ -4802,7 +4810,10 @@ async def _call_fallback_candidate_async(
temperature=temperature, max_tokens=max_tokens,
tools=fallback_tools, timeout=effective_timeout,
extra_body=effective_extra_body, reasoning_config=reasoning_config,
base_url=destination.base_url, task=task)
base_url=destination.base_url,
task=task,
api_key=getattr(fb_client, "api_key", None),
)
try:
return _validate_llm_response(
await _relay_async_completion(
Expand Down Expand Up @@ -4848,7 +4859,10 @@ async def _call_fallback_candidate_async(
tools=retry_tools, timeout=effective_timeout,
extra_body=effective_extra_body,
reasoning_config=reasoning_config,
base_url=retry_destination.base_url, task=task)
base_url=retry_destination.base_url,
task=task,
api_key=getattr(retry_client, "api_key", None),
)
try:
return _validate_llm_response(
await _relay_async_completion(
Expand Down Expand Up @@ -7745,6 +7759,7 @@ def _build_call_kwargs(
reasoning_config: Optional[dict] = None,
base_url: Optional[str] = None,
task: Optional[str] = None,
api_key: Optional[str] = None,
) -> dict:
"""Build kwargs for .chat.completions.create() with model/provider adjustments."""
kwargs: Dict[str, Any] = {
Expand Down Expand Up @@ -7885,6 +7900,7 @@ def _build_call_kwargs(
supports_reasoning=reasoning_config is not None,
model=model,
base_url=effective_base,
api_key=api_key if isinstance(api_key, str) else "",
)
)
profile_reasoning_extra = profile_reasoning_extra or {}
Expand Down Expand Up @@ -8597,7 +8613,8 @@ def call_llm(
temperature=temperature, max_tokens=max_tokens,
tools=tools, timeout=effective_timeout, extra_body=effective_extra_body,
reasoning_config=reasoning_config,
base_url=_base_info or resolved_base_url, task=task)
base_url=_base_info or resolved_base_url, task=task,
api_key=resolved_api_key or getattr(client, "api_key", None))
if extra_headers:
kwargs["extra_headers"] = dict(extra_headers)

Expand Down Expand Up @@ -9309,7 +9326,8 @@ async def async_call_llm(
temperature=temperature, max_tokens=max_tokens,
tools=tools, timeout=effective_timeout, extra_body=effective_extra_body,
reasoning_config=reasoning_config,
base_url=_client_base or resolved_base_url, task=task)
base_url=_client_base or resolved_base_url, task=task,
api_key=resolved_api_key or getattr(client, "api_key", None))

# Convert image blocks for Anthropic-compatible endpoints (e.g. MiniMax)
if _is_anthropic_compat_endpoint(resolved_provider, _client_base):
Expand Down
1 change: 1 addition & 0 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,7 @@ def build_api_kwargs(agent, api_messages: list, tools_for_api: list | None = Non
request_overrides=agent.request_overrides,
session_id=getattr(agent, "session_id", None),
provider_profile=_profile,
api_key=agent.api_key if isinstance(agent.api_key, str) else "",
ollama_num_ctx=agent._ollama_num_ctx,
# Context forwarded to profile hooks:
provider_preferences=_prefs or None,
Expand Down
1 change: 1 addition & 0 deletions agent/transports/chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ def _build_kwargs_from_profile(self, profile, model, sanitized, tools, params):
qwen_session_metadata=params.get("qwen_session_metadata"),
model=model,
base_url=params.get("base_url"),
api_key=params.get("api_key"),
ollama_num_ctx=params.get("ollama_num_ctx"),
session_id=params.get("session_id"),
)
Expand Down
63 changes: 37 additions & 26 deletions plugins/model-providers/custom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
Ollama instances and OpenAI-compatible reasoning endpoints (GLM-5.2 on
Volcengine ARK, vLLM, llama.cpp). Key quirks:
- ollama_num_ctx → extra_body.options.num_ctx (local context window)
- reasoning_config disabled → top-level reasoning_effort="none"
(Ollama /v1/chat/completions ignores think=False — ollama#14820)
+ extra_body.think = False for /api/chat and proxies
- reasoning_config enabled + effort → top-level reasoning_effort
(the native OpenAI-compatible format GLM/ARK expect; unset omits it
so the endpoint's server default applies)
- reasoning_config disabled on a verified Ollama endpoint → top-level
reasoning_effort="none" + extra_body.think = False
- reasoning_config effort → top-level reasoning_effort on a verified
Ollama endpoint only (unset omits it so the server default applies)
"""

from typing import Any
Expand All @@ -18,6 +16,18 @@
from providers.base import ProviderProfile


def _is_verified_ollama_endpoint(base_url: str | None, api_key: str = "") -> bool:
"""Return true only when the endpoint passes the shared Ollama probe."""
if not base_url:
return False
try:
from agent.model_metadata import detect_local_server_type

return detect_local_server_type(base_url, api_key=api_key) == "ollama"
except Exception:
return False


class CustomProfile(ProviderProfile):
"""Custom/Ollama local provider — think=false and num_ctx support."""

Expand All @@ -37,32 +47,33 @@ def build_api_kwargs_extras(
options["num_ctx"] = ollama_num_ctx
extra_body["options"] = options

# Reasoning / thinking control for custom OpenAI-compatible endpoints
# (GLM-5.2 on Volcengine ARK, vLLM, Ollama, llama.cpp, …).
# The disabled-reasoning controls are Ollama-specific. ``custom`` is
# also used for arbitrary OpenAI-compatible relays (including Groq),
# which can reject either ``think=False`` or
# ``reasoning_effort="none"``. Do not infer Ollama from a URL
# substring or its default port: emit those fields only after the
# shared endpoint probe identifies Ollama.
#
# - disabled → extra_body.think = False (Ollama's thinking-off flag)
# - enabled + effort set → TOP-LEVEL reasoning_effort string, the
# format GLM-5.2/ARK and other OpenAI-compatible reasoning APIs
# expect (GLM documents "high" and "max"; "max" is its default).
# - enabled + no effort → omit both, so the endpoint applies its own
# server-side default (do NOT force a level the user didn't pick).
# - disabled → reasoning_effort="none" and extra_body.think=False
# - enabled + effort → top-level reasoning_effort
# - enabled + no effort → omit both, preserving the server default
#
# We deliberately do NOT emit ``think=True`` on enable: it is an
# Ollama-only flag and thinking is already server-default-on for these
# backends, so forcing it risks a 400 on GLM/vLLM endpoints that don't
# recognize it. Mirrors the DeepSeek/Zai profile precedent.
# Enabled ``reasoning_effort`` remains a generic OpenAI-compatible
# control used by custom reasoning APIs such as GLM/ARK. We
# deliberately do NOT emit ``think=True`` on enable.
if reasoning_config and isinstance(reasoning_config, dict):
_effort = (reasoning_config.get("effort") or "").strip().lower()
_enabled = reasoning_config.get("enabled", True)
if _effort == "none" or _enabled is False:
# Ollama's /v1/chat/completions silently ignores
# extra_body.think (only /api/chat honours it — ollama#14820)
# but respects the top-level reasoning_effort field, so both
# are needed to actually stop a thinking-capable model from
# reasoning (#25758). Endpoints that recognize neither simply
# ignore them.
top_level["reasoning_effort"] = "none"
extra_body["think"] = False
if _is_verified_ollama_endpoint(
ctx.get("base_url"), str(ctx.get("api_key") or "")
):
# Ollama's /v1/chat/completions silently ignores
# extra_body.think (only /api/chat honours it —
# ollama#14820) but respects the top-level field, so both
# are needed to stop a thinking-capable model (#25758).
top_level["reasoning_effort"] = "none"
extra_body["think"] = False
elif _effort:
top_level["reasoning_effort"] = _effort

Expand Down
39 changes: 39 additions & 0 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2739,6 +2739,45 @@ def test_kimi_reasoning_uses_top_level_effort(self):
assert "thinking" not in kwargs.get("extra_body", {})


def test_custom_groq_endpoint_omits_ollama_reasoning_controls(self, monkeypatch):
monkeypatch.setattr(
"agent.model_metadata.detect_local_server_type", lambda *_args, **_kwargs: None
)
kwargs = _build_call_kwargs(
"custom",
"llama-3.3-70b-versatile",
[{"role": "user", "content": "hi"}],
reasoning_config={"enabled": False, "effort": "none"},
base_url="https://api.groq.com/openai/v1",
)

assert "reasoning_effort" not in kwargs
assert "reasoning" not in kwargs.get("extra_body", {})
assert "think" not in kwargs.get("extra_body", {})

def test_custom_verified_ollama_uses_auxiliary_api_key_for_disabled_reasoning(self, monkeypatch):
probe_calls = []

def _is_ollama(url, *, api_key=""):
probe_calls.append((url, api_key))
return "ollama"

monkeypatch.setattr(
"agent.model_metadata.detect_local_server_type", _is_ollama,
)
base_url = "http://127.0.0.1:11434/v1"
kwargs = _build_call_kwargs(
"custom",
"qwen3",
[{"role": "user", "content": "hi"}],
reasoning_config={"enabled": False},
base_url=base_url,
api_key="auxiliary-test-key",
)

assert probe_calls == [(base_url, "auxiliary-test-key")]
assert kwargs["reasoning_effort"] == "none"
assert kwargs["extra_body"]["think"] is False

@pytest.mark.asyncio
async def test_async_call_llm_preserves_profile_reasoning_kwargs(self):
Expand Down
24 changes: 24 additions & 0 deletions tests/agent/test_probe_cache_followups.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,30 @@ def _get(url, *a, **k):
with patch("httpx.Client", return_value=swap_client):
assert detect_local_server_type("http://127.0.0.1:11434") == "lm-studio"

@pytest.mark.parametrize(
"base_url",
[
"https://not-ollama.example/v1",
"http://127.0.0.1:11434/v1",
],
ids=["url-containing-ollama", "non-ollama-port-11434"],
)
def test_non_ollama_name_or_port_does_not_identify_ollama(self, base_url):
"""Ollama requires the /api/tags response shape, not URL heuristics."""
from agent.model_metadata import detect_local_server_type

miss = MagicMock()
miss.status_code = 404
client = MagicMock()
client.__enter__ = lambda s: client
client.__exit__ = MagicMock(return_value=False)
client.get.return_value = miss

with patch("httpx.Client", return_value=client):
assert detect_local_server_type(base_url) is None

assert any(call.args[0].endswith("/api/tags") for call in client.get.call_args_list)


class TestLocalhostIPv4SiblingSites:
"""#37595 widened: every probe helper rewrites localhost→127.0.0.1,
Expand Down
7 changes: 6 additions & 1 deletion tests/agent/transports/test_chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,21 @@ def test_ollama_num_ctx(self, transport):
)
assert kw["extra_body"]["options"]["num_ctx"] == 32768

def test_custom_think_false(self, transport):
def test_custom_verified_ollama_sends_disabled_controls(self, transport, monkeypatch):
from providers import get_provider_profile
monkeypatch.setattr(
"agent.model_metadata.detect_local_server_type", lambda *_args, **_kwargs: "ollama"
)
profile = get_provider_profile("custom")
msgs = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs(
model="qwen3", messages=msgs,
provider_profile=profile,
base_url="http://127.0.0.1:11434/v1",
reasoning_config={"effort": "none"},
)
assert kw["extra_body"]["think"] is False
assert kw["reasoning_effort"] == "none"



Expand Down
Loading