Skip to content
Open
8 changes: 8 additions & 0 deletions anton/cloud_turn/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,20 @@ class TurnRequestV1:
#: {engine: {"usage_notes": str}}. Rendered only for engines the turn's
#: vault connects. Absent when no connected engine has notes.
connectors: dict | None = None
#: Optional reasoning effort the user picked for this turn. Applied to the
#: planning model only; the name is passed to the provider uninterpreted.
#: Absent, null, blank, or non-string keeps the configured default.
reasoning_effort: str | None = None

@staticmethod
def from_json(raw: str) -> "TurnRequestV1":
d = json.loads(raw)
protocol_version = d["protocol_version"]
if not _is_version(protocol_version, TURN_PROTOCOL_VERSION):
raise ValueError("unsupported cloud turn protocol version")
reasoning_effort = d.get("reasoning_effort")
if not isinstance(reasoning_effort, str):
reasoning_effort = ""
return TurnRequestV1(
protocol_version=protocol_version,
conversation_id=str(d["conversation_id"]),
Expand Down Expand Up @@ -260,4 +267,5 @@ def from_json(raw: str) -> "TurnRequestV1":
connectors=(
d.get("connectors") if isinstance(d.get("connectors"), dict) else None
),
reasoning_effort=reasoning_effort.strip() or None,
)
4 changes: 4 additions & 0 deletions anton/cloud_turn/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,10 @@ def build_cloud_chat_session(
settings.resolve_workspace(str(base))
if request.model:
settings.planning_model = request.model
# Planning only: the router shares the coding provider, so changing coding
# effort would also change history summarization.
if request.reasoning_effort:
settings.planning_reasoning_effort = request.reasoning_effort
# Mounted (_SKILLS_ROOT_ENV set): the organization's own live skills tree,
# owned by cowork-server, read directly. Unmounted (desktop, CI): the
# request's skills are staged outside the workspace instead, since that PVC
Expand Down
22 changes: 22 additions & 0 deletions tests/test_cloud_turn_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,25 @@ def test_a_missing_or_non_object_connectors_block_parses_to_none(value):
if value is not None:
body["connectors"] = value
assert TurnRequestV1.from_json(json.dumps(body)).connectors is None


def _with_effort(**extra):
"""The parsed request for a minimal body carrying ``extra``."""
body = {"protocol_version": 1, "conversation_id": "c", "input": "hi"}
body.update(extra)
return TurnRequestV1.from_json(json.dumps(body))


@pytest.mark.parametrize("effort", ["low", "none", "high", "some-future-level"])
def test_reasoning_effort_is_passed_through_uninterpreted(effort):
assert _with_effort(reasoning_effort=effort).reasoning_effort == effort


@pytest.mark.parametrize("extra", [{}, {"reasoning_effort": None}, {"reasoning_effort": ""}, {"reasoning_effort": " "}])
def test_absent_null_or_blank_reasoning_effort_is_unset(extra):
assert _with_effort(**extra).reasoning_effort is None


@pytest.mark.parametrize("effort", [1, True, ["high"], {"level": "high"}, 0.5])
def test_a_non_string_reasoning_effort_falls_back_to_the_default(effort):
assert _with_effort(reasoning_effort=effort).reasoning_effort is None
120 changes: 120 additions & 0 deletions tests/test_cloud_turn_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
# `_pin_today` later replaces on the module.
from datetime import datetime as _stdlib_datetime

import httpx2 as httpx
import openai
import pytest

import anton.core.llm.client as llm_client_mod
Expand Down Expand Up @@ -1261,6 +1263,124 @@ def fake_minds_request(url, api_key, *, method="GET", payload=None, verify=True,
assert "DS_GMAIL_PRIMARY__ACCESS_TOKEN" not in os.environ


_MINDS_LLM = {"provider": "minds-cloud", "api_key": "mdb_k", "base_url": "https://api.mindshub.ai"}


def _build_real_client(tmp_path, monkeypatch, **req_overrides):
"""The real LLMClient a cloud turn builds for this request, with ChatSession faked."""
captured: dict = {}
real_from_settings = llm_client_mod.LLMClient.from_settings.__func__

def capture(cls, settings):
captured["client"] = real_from_settings(cls, settings)
return captured["client"]

monkeypatch.setattr(session_mod, "ChatSession", lambda config: _FakeSession())
monkeypatch.setattr(llm_client_mod.LLMClient, "from_settings", classmethod(capture))
monkeypatch.setenv(_WORKSPACE_PATH_ENV, str(tmp_path))
body = dict(protocol_version=1, conversation_id="conv_1", input="hello", llm=_MINDS_LLM)
body.update(req_overrides)
build_cloud_chat_session(TurnRequestV1(**body))
return captured["client"]


def _chat_stream(text: str) -> bytes:
"""``text`` as a streamed chat.completions answer, the format the minds passthrough reads."""
chunk = {"id": "chatcmpl-1", "object": "chat.completion.chunk", "created": 0, "model": "m"}
chunks = [
{**chunk, "choices": [{"index": 0, "delta": {"role": "assistant", "content": text},
"finish_reason": None}]},
{**chunk, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
]
return ("".join(f"data: {json.dumps(c)}\n\n" for c in chunks) + "data: [DONE]\n\n").encode()


def _record_requests(monkeypatch) -> list[tuple[str, dict]]:
"""Point every OpenAI SDK client built from now on at a transport that
records each request as (path, body) and answers it with a short completion."""
requests: list[tuple[str, dict]] = []

def handler(request: httpx.Request) -> httpx.Response:
sent = json.loads(request.content)
requests.append((request.url.path, sent))
if sent.get("stream"):
return httpx.Response(
200, headers={"content-type": "text/event-stream"},
content=_chat_stream("answered"),
)
return httpx.Response(200, json={
"id": "chatcmpl-1", "object": "chat.completion", "created": 0, "model": "m",
"choices": [{"index": 0, "finish_reason": "stop",
"message": {"role": "assistant", "content": "answered"}}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
})

real_async_openai = openai.AsyncOpenAI

def mock_async_openai(**kwargs):
kwargs["http_client"] = httpx.AsyncClient(transport=httpx.MockTransport(handler))
return real_async_openai(**{**kwargs, "max_retries": 0})

monkeypatch.setattr(openai, "AsyncOpenAI", mock_async_openai)
return requests


async def _efforts_on_the_wire(client, requests) -> dict[str, str | None]:
"""One call per role; the ``reasoning_effort`` each chat.completions request carried."""
messages = [{"role": "user", "content": "hi"}]
async for _ in client.plan_stream(system="s", messages=messages):
pass
async for _ in client.code_stream(system="s", messages=messages):
pass
await client.summarize(system="s", messages=messages)
assert [path for path, _ in requests] == ["/v1/chat/completions"] * 3
efforts = (sent.get("reasoning_effort") for _, sent in requests)
return dict(zip(("planning", "coding", "router"), efforts))


@pytest.mark.parametrize("coding_model", [None, "claude-opus-4-6"])
@pytest.mark.parametrize("router", [False, True])
async def test_selected_effort_reaches_only_the_planning_request(tmp_path, monkeypatch, coding_model, router):
"""The router shares the coding provider unless one is configured, so
coding effort must stay put or history summarization would change too."""
if router:
monkeypatch.setenv("ANTON_ROUTER_PROVIDER", "openai-compatible")
monkeypatch.setenv("ANTON_CODING_REASONING_EFFORT", "medium")
requests = _record_requests(monkeypatch)
llm = dict(_MINDS_LLM, coding_model=coding_model) if coding_model else _MINDS_LLM
client = _build_real_client(
tmp_path, monkeypatch, model="claude-opus-4-6", llm=llm, reasoning_effort="low",
)
try:
sent = await _efforts_on_the_wire(client, requests)
finally:
await client.aclose()
assert sent == {"planning": "low", "coding": "medium", "router": None if router else "medium"}


async def test_no_selected_effort_keeps_the_configured_planning_effort(tmp_path, monkeypatch):
"""With nothing selected, planning carries the configured effort and the other roles none."""
monkeypatch.setenv("ANTON_PLANNING_REASONING_EFFORT", "high")
requests = _record_requests(monkeypatch)
client = _build_real_client(tmp_path, monkeypatch, reasoning_effort=None)
try:
sent = await _efforts_on_the_wire(client, requests)
finally:
await client.aclose()
assert sent == {"planning": "high", "coding": None, "router": None}


async def test_no_selected_or_configured_effort_sends_none(tmp_path, monkeypatch):
"""With nothing selected or configured, no request carries an effort."""
requests = _record_requests(monkeypatch)
client = _build_real_client(tmp_path, monkeypatch)
try:
sent = await _efforts_on_the_wire(client, requests)
finally:
await client.aclose()
assert sent == {"planning": None, "coding": None, "router": None}


# ── ENG-1638: the pod injects a configured-LLM block ─────────────────────────

def test_pod_injects_a_configured_llm_block_for_the_runtime_identity(tmp_path, monkeypatch):
Expand Down
Loading