Skip to content
Merged
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
10 changes: 10 additions & 0 deletions services/convsim-core/convsim_core/runtime/sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,22 @@ async def start(
log_fh.write(header)
log_fh.flush()

# NPC turns need fast, in-character JSON — not visible chain-of-thought.
# Reasoning-family models (Qwen3, DeepSeek-R1 distils) otherwise spend
# hundreds of tokens thinking, blowing the turn token budget (truncated
# JSON → fallback utterance) and multiplying latency. LLAMA_ARG_REASONING
# is honoured by current llama-server builds and silently ignored by
# older ones, unlike the equivalent CLI flag which would fail startup.
env = dict(os.environ)
env.setdefault("LLAMA_ARG_REASONING", "off")

try:
self._process = await asyncio.create_subprocess_exec(
*cmd,
stdout=log_fh,
stderr=subprocess.STDOUT,
creationflags=CREATE_NO_WINDOW,
env=env,
)
except OSError as exc:
self._state = SidecarState.CRASHED
Expand Down
6 changes: 6 additions & 0 deletions services/convsim-core/convsim_core/services/turn_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,12 @@ async def process_turn(
messages=messages,
json_schema=NPC_TURN_OUTPUT_SCHEMA,
scripted_turn_index=turn_number,
# Structured turn output (utterance + deltas + rubric observations)
# regularly lands in the 300-600 token range; the 512 default left no
# headroom and a truncated JSON object degrades to the fallback
# utterance. Reasoning is disabled at the engine level, so the extra
# budget costs nothing when unused.
max_tokens=1024,
)
logger.debug(
"Calling runtime %s for session %s turn %d (estimated %d tokens, truncated=%s)",
Expand Down
47 changes: 47 additions & 0 deletions services/convsim-core/tests/test_sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1037,3 +1037,50 @@ async def test_structured_output_via_fake_server(tmp_path):

finally:
await sidecar.stop()


@pytest.mark.asyncio
async def test_start_disables_model_reasoning_via_env(tmp_path, monkeypatch):
"""The spawned llama-server must run with LLAMA_ARG_REASONING=off.

Reasoning-family models (Qwen3, R1 distils) otherwise burn the turn token
budget on chain-of-thought: the structured turn JSON gets truncated, every
turn degrades to the fallback utterance, and latency multiplies. The env
var (unlike the CLI flag) is ignored by older llama-server binaries, so
user-installed engines keep starting.
"""
from convsim_core.runtime.sidecar import LlamaCppSidecar

captured: dict = {}

async def _fake_exec(*args, **kwargs):
captured["env"] = kwargs.get("env")
raise OSError("stop after capture")

monkeypatch.setattr("asyncio.create_subprocess_exec", _fake_exec)
sidecar = LlamaCppSidecar(log_dir=str(tmp_path))
with pytest.raises(RuntimeError):
await sidecar.start("/tmp/model.gguf", executable="/tmp/llama-server")

assert captured["env"] is not None
assert captured["env"].get("LLAMA_ARG_REASONING") == "off"


@pytest.mark.asyncio
async def test_start_respects_existing_reasoning_env(tmp_path, monkeypatch):
"""An explicit user override of LLAMA_ARG_REASONING wins over the default."""
from convsim_core.runtime.sidecar import LlamaCppSidecar

captured: dict = {}

async def _fake_exec(*args, **kwargs):
captured["env"] = kwargs.get("env")
raise OSError("stop after capture")

monkeypatch.setenv("LLAMA_ARG_REASONING", "on")
monkeypatch.setattr("asyncio.create_subprocess_exec", _fake_exec)
sidecar = LlamaCppSidecar(log_dir=str(tmp_path))
with pytest.raises(RuntimeError):
await sidecar.start("/tmp/model.gguf", executable="/tmp/llama-server")

assert captured["env"].get("LLAMA_ARG_REASONING") == "on"
Loading