From ace84b157d0115219ffef2eb508649d2515a194a Mon Sep 17 00:00:00 2001 From: jiawei Date: Thu, 17 Sep 2026 15:12:04 +0800 Subject: [PATCH 1/2] fix(proxy): preserve configured chat template defaults --- .../openai/proxy/proxy_rollout_server.py | 24 +++++++++ tests/test_proxy_chat_template_defaults.py | 52 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_proxy_chat_template_defaults.py diff --git a/areal/experimental/openai/proxy/proxy_rollout_server.py b/areal/experimental/openai/proxy/proxy_rollout_server.py index 9eb8994bb7..47710a2cea 100644 --- a/areal/experimental/openai/proxy/proxy_rollout_server.py +++ b/areal/experimental/openai/proxy/proxy_rollout_server.py @@ -900,6 +900,30 @@ async def _call_client_create( raise HTTPException(status_code=500, detail=message) from e kwargs["messages"] = prepared_messages + defaults = dict( + getattr(_engine.config.agent, "chat_template_kwargs", {}) if _engine else {} + ) + extra_body = dict(kwargs.get("extra_body") or {}) + session_template = session_data.metadata.get("chat_template_kwargs") or {} + request_template = { + **session_template, + **(extra_body.get("chat_template_kwargs") or {}), + **(kwargs.pop("chat_template_kwargs", None) or {}), + } + thinking_keys = ("thinking_option", "enable_thinking", "thinking") + session_thinking = {k: v for k, v in session_template.items() if k in thinking_keys} + if session_thinking: + for key in thinking_keys: + request_template.pop(key, None) + request_template.update(session_thinking) + if any(key in request_template for key in thinking_keys): + for key in thinking_keys: + defaults.pop(key, None) + template_kwargs = {**defaults, **request_template} + if template_kwargs: + extra_body["chat_template_kwargs"] = template_kwargs + kwargs["extra_body"] = extra_body + dropped_args = [] for k, v in kwargs.items(): if k not in areal_client_allowed_args: diff --git a/tests/test_proxy_chat_template_defaults.py b/tests/test_proxy_chat_template_defaults.py new file mode 100644 index 0000000000..98339b70b9 --- /dev/null +++ b/tests/test_proxy_chat_template_defaults.py @@ -0,0 +1,52 @@ +from types import SimpleNamespace + +import pytest + +from areal.experimental.openai.proxy import proxy_rollout_server as server +from areal.experimental.openai.proxy.server import SessionData + + +@pytest.mark.asyncio +@pytest.mark.parametrize("override", [None, False, "off"]) +async def test_proxy_preserves_template_defaults_and_request_overrides( + monkeypatch, override +): + defaults = { + "enable_thinking": True, + "reasoning_effort": "medium", + "thinking_option": None, + } + monkeypatch.setattr( + server, + "_engine", + SimpleNamespace( + config=SimpleNamespace(agent=SimpleNamespace(chat_template_kwargs=defaults)) + ), + ) + monkeypatch.setattr(server, "_openai_client", object()) + monkeypatch.setattr(server, "_session_cache", {"test": SessionData("test")}) + monkeypatch.setattr(server, "_message_preprocessors", []) + monkeypatch.setattr(server, "_deterministic_sampling", False) + request = {"messages": [], "extra_body": {"other": "kept"}} + if override is not None: + request["chat_template_kwargs"] = ( + {"thinking_option": "off"} + if override == "off" + else {"enable_thinking": override} + ) + + async def create(messages, extra_body, temperature, top_p, areal_cache): + return extra_body + + result = await server._call_client_create(create, request, "test") + if override == "off": + assert result["chat_template_kwargs"]["thinking_option"] == "off" + assert "enable_thinking" not in result["chat_template_kwargs"] + else: + assert result["chat_template_kwargs"]["enable_thinking"] is ( + True if override is None else override + ) + assert result["chat_template_kwargs"]["reasoning_effort"] == "medium" + assert result["other"] == "kept" + assert defaults["enable_thinking"] is True + assert request["extra_body"] == {"other": "kept"} From e27d251450f66e1b32e802d0ccd402a63dde70b6 Mon Sep 17 00:00:00 2001 From: jiawei Date: Fri, 18 Sep 2026 11:33:42 +0800 Subject: [PATCH 2/2] fix(proxy): expose defaults and order thinking aliases --- areal/api/cli_args.py | 4 +++ .../openai/proxy/proxy_rollout_server.py | 36 ++++++++++++------- docs/en/cli_reference.md | 1 + docs/zh/cli_reference.md | 1 + 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index 50b619ea80..76a48d1261 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -2624,6 +2624,10 @@ class AgentConfig: default="qwen3", metadata={"help": "Parser for reasoning content ( tags)."}, ) + chat_template_kwargs: dict[str, Any] = field( + default_factory=dict, + metadata={"help": "Default chat template arguments for proxy requests."}, + ) chat_template_type: str = field( default="hf", metadata={ diff --git a/areal/experimental/openai/proxy/proxy_rollout_server.py b/areal/experimental/openai/proxy/proxy_rollout_server.py index 47710a2cea..531d02d3d6 100644 --- a/areal/experimental/openai/proxy/proxy_rollout_server.py +++ b/areal/experimental/openai/proxy/proxy_rollout_server.py @@ -905,21 +905,33 @@ async def _call_client_create( ) extra_body = dict(kwargs.get("extra_body") or {}) session_template = session_data.metadata.get("chat_template_kwargs") or {} - request_template = { - **session_template, - **(extra_body.get("chat_template_kwargs") or {}), - **(kwargs.pop("chat_template_kwargs", None) or {}), - } thinking_keys = ("thinking_option", "enable_thinking", "thinking") - session_thinking = {k: v for k, v in session_template.items() if k in thinking_keys} + template_kwargs = {} + for layer in ( + defaults, + session_template, + extra_body.get("chat_template_kwargs") or {}, + kwargs.pop("chat_template_kwargs", None) or {}, + ): + effective = { + key: value + for key, value in layer.items() + if key not in thinking_keys or value is not None + } + # Thinking aliases share precedence, even when their names differ. + if any(key in effective for key in thinking_keys): + for key in thinking_keys: + template_kwargs.pop(key, None) + template_kwargs.update(effective) + session_thinking = { + key: value + for key, value in session_template.items() + if key in thinking_keys and value is not None + } if session_thinking: for key in thinking_keys: - request_template.pop(key, None) - request_template.update(session_thinking) - if any(key in request_template for key in thinking_keys): - for key in thinking_keys: - defaults.pop(key, None) - template_kwargs = {**defaults, **request_template} + template_kwargs.pop(key, None) + template_kwargs.update(session_thinking) if template_kwargs: extra_body["chat_template_kwargs"] = template_kwargs kwargs["extra_body"] = extra_body diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index 5e5925dea1..6f5c5dc4c1 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -948,6 +948,7 @@ Consolidates proxy settings (mode, parsers, export) with agent-service orchestra | `mode` | string | `"inline"` | OpenAI proxy mode: 'inline' (in-process), 'subproc' (subprocess), or 'online' (external user sessions for online RL training). `inline` mode runs the provided agent workflow directly in the same process. `subproc` mode launches a separate process to run the agent. `online` mode waits for external users to complete sessions via the proxy gateway URL, enabling online RL training. **Choices:** `inline`, `subproc`, `online` | | `tool_call_parser` | string | `"qwen"` | Parser for tool calls in model output. | | `reasoning_parser` | string | `"qwen3"` | Parser for reasoning content ( tags). | +| `chat_template_kwargs` | `dict` | `{}` | Default chat template arguments for proxy requests. | | `chat_template_type` | string | `"hf"` | Chat template type: 'hf' (standard) or 'concat' (multi-turn concatenation). **Choices:** `hf`, `concat` | | `engine_max_tokens` | integer \| None | `None` | Maximum total tokens for the engine (prompt + completion). | | `turn_discount` | float | `1.0` | Discount factor for reward propagation in 'individual' export. Concat leaves keep their own branch-local outcome reward. | diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index 8ce06cfab6..5c0b911a74 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -946,6 +946,7 @@ Consolidates proxy settings (mode, parsers, export) with agent-service orchestra | `mode` | string | `"inline"` | OpenAI proxy mode: 'inline' (in-process), 'subproc' (subprocess), or 'online' (external user sessions for online RL training). `inline` mode runs the provided agent workflow directly in the same process. `subproc` mode launches a separate process to run the agent. `online` mode waits for external users to complete sessions via the proxy gateway URL, enabling online RL training. **Choices:** `inline`, `subproc`, `online` | | `tool_call_parser` | string | `"qwen"` | Parser for tool calls in model output. | | `reasoning_parser` | string | `"qwen3"` | Parser for reasoning content ( tags). | +| `chat_template_kwargs` | `dict` | `{}` | Default chat template arguments for proxy requests. | | `chat_template_type` | string | `"hf"` | Chat template type: 'hf' (standard) or 'concat' (multi-turn concatenation). **Choices:** `hf`, `concat` | | `engine_max_tokens` | integer \| None | `None` | Maximum total tokens for the engine (prompt + completion). | | `turn_discount` | float | `1.0` | Discount factor for reward propagation in 'individual' export. Concat leaves keep their own branch-local outcome reward. |