11"""OpenAI-compatible Qwen chat backend for optimizer and target paths."""
2+
23from __future__ import annotations
34
4- from dataclasses import dataclass
55import json
66import os
77import threading
88import time
99import urllib .error
1010import urllib .request
11+ from dataclasses import dataclass
1112from typing import Any
1213
1314from skillopt .model .common import (
@@ -28,6 +29,7 @@ class QwenChatConfig:
2829 temperature : float | None
2930 enable_thinking : bool
3031 deployment : str
32+ use_max_completion_tokens : bool = False
3133
3234
3335def _parse_bool (value : Any , default : bool = False ) -> bool :
@@ -56,6 +58,28 @@ def _role_env(role: str, key: str, default: str) -> str:
5658 return os .environ .get (role_key ) or os .environ .get (generic_key ) or default
5759
5860
61+ # Sentinels that mean "omit this optional parameter from the request payload".
62+ # Reasoning models (e.g. GPT-5.x, Claude Opus 4.8) reject an explicit
63+ # `temperature`, so allow it to be turned off via an empty string / none / off.
64+ _OMIT_SENTINELS = {"" , "none" , "off" , "null" }
65+
66+
67+ def _resolve_temperature (role : str ) -> float | None :
68+ """Return the temperature, or None to omit it entirely.
69+
70+ Unlike ``_role_env`` an *explicitly set* empty (or ``none``/``off``) value is
71+ honored as "omit" instead of collapsing to the default. Precedence:
72+ role-specific env -> generic env -> 0.7 default.
73+ """
74+ for key in (f"{ role .upper ()} _QWEN_CHAT_TEMPERATURE" , "QWEN_CHAT_TEMPERATURE" ):
75+ if key in os .environ :
76+ raw = os .environ [key ].strip ()
77+ if raw .lower () in _OMIT_SENTINELS :
78+ return None
79+ return float (raw )
80+ return 0.7
81+
82+
5983def _initial_config (role : str ) -> QwenChatConfig :
6084 role_upper = role .upper ()
6185 deployment_env = "OPTIMIZER_DEPLOYMENT" if role == "optimizer" else "TARGET_DEPLOYMENT"
@@ -64,8 +88,9 @@ def _initial_config(role: str) -> QwenChatConfig:
6488 api_key = _role_env (role , "API_KEY" , "" ),
6589 timeout_seconds = float (_role_env (role , "TIMEOUT_SECONDS" , "300" ) or 300 ),
6690 max_tokens = _parse_int (_role_env (role , "MAX_TOKENS" , "8000" ), 8000 ),
67- temperature = _parse_optional_float ( _role_env ( role , "TEMPERATURE" , "0.7" ) ),
91+ temperature = _resolve_temperature ( role ),
6892 enable_thinking = _parse_bool (_role_env (role , "ENABLE_THINKING" , "false" )),
93+ use_max_completion_tokens = _parse_bool (_role_env (role , "USE_MAX_COMPLETION_TOKENS" , "false" )),
6994 deployment = (
7095 os .environ .get (f"{ role_upper } _QWEN_CHAT_MODEL" )
7196 or os .environ .get ("QWEN_CHAT_MODEL" )
@@ -186,10 +211,14 @@ def _chat_messages_impl(
186211 timeout : float | None = None ,
187212) -> tuple [Any , dict [str , int ]]:
188213 config = OPTIMIZER_CONFIG if role == "optimizer" else TARGET_CONFIG
214+ token_limit = min (max_completion_tokens , config .max_tokens )
215+ # Reasoning models on some gateways (GPT-5.x, o-series) require
216+ # `max_completion_tokens` and reject the legacy `max_tokens`.
217+ token_key = "max_completion_tokens" if config .use_max_completion_tokens else "max_tokens"
189218 payload : dict [str , Any ] = {
190219 "model" : deployment or config .deployment ,
191220 "messages" : _json_safe (messages ),
192- "max_tokens" : min ( max_completion_tokens , config . max_tokens ) ,
221+ token_key : token_limit ,
193222 }
194223 if config .enable_thinking :
195224 payload ["chat_template_kwargs" ] = {"enable_thinking" : True }
@@ -219,7 +248,7 @@ def _chat_messages_impl(
219248 return text , usage_info
220249 except Exception as e : # noqa: BLE001
221250 last_err = e
222- time .sleep (min (2 ** attempt , 30 ))
251+ time .sleep (min (2 ** attempt , 30 ))
223252 raise RuntimeError (f"Qwen chat call failed after { retries } retries: { last_err } " )
224253
225254
@@ -231,18 +260,21 @@ def configure_qwen_chat(
231260 timeout_seconds : float | str | None = None ,
232261 max_tokens : int | str | None = None ,
233262 enable_thinking : bool | str | None = None ,
263+ use_max_completion_tokens : bool | str | None = None ,
234264 optimizer_base_url : str | None = None ,
235265 optimizer_api_key : str | None = None ,
236266 optimizer_temperature : float | str | None = None ,
237267 optimizer_timeout_seconds : float | str | None = None ,
238268 optimizer_max_tokens : int | str | None = None ,
239269 optimizer_enable_thinking : bool | str | None = None ,
270+ optimizer_use_max_completion_tokens : bool | str | None = None ,
240271 target_base_url : str | None = None ,
241272 target_api_key : str | None = None ,
242273 target_temperature : float | str | None = None ,
243274 target_timeout_seconds : float | str | None = None ,
244275 target_max_tokens : int | str | None = None ,
245276 target_enable_thinking : bool | str | None = None ,
277+ target_use_max_completion_tokens : bool | str | None = None ,
246278) -> None :
247279 with _config_lock :
248280 if base_url is not None :
@@ -256,29 +288,24 @@ def configure_qwen_chat(
256288 if max_tokens is not None :
257289 os .environ ["QWEN_CHAT_MAX_TOKENS" ] = str (max_tokens )
258290 if enable_thinking is not None :
259- os .environ ["QWEN_CHAT_ENABLE_THINKING" ] = (
260- "true" if _parse_bool (enable_thinking ) else "false"
291+ os .environ ["QWEN_CHAT_ENABLE_THINKING" ] = "true" if _parse_bool (enable_thinking ) else "false"
292+ if use_max_completion_tokens is not None :
293+ os .environ ["QWEN_CHAT_USE_MAX_COMPLETION_TOKENS" ] = (
294+ "true" if _parse_bool (use_max_completion_tokens ) else "false"
261295 )
262296 _update_config (
263297 OPTIMIZER_CONFIG ,
264298 "optimizer" ,
265299 base_url = optimizer_base_url if optimizer_base_url is not None else base_url ,
266300 api_key = optimizer_api_key if optimizer_api_key is not None else api_key ,
267- temperature = (
268- optimizer_temperature
269- if optimizer_temperature is not None
270- else temperature
271- ),
272- timeout_seconds = (
273- optimizer_timeout_seconds
274- if optimizer_timeout_seconds is not None
275- else timeout_seconds
276- ),
301+ temperature = (optimizer_temperature if optimizer_temperature is not None else temperature ),
302+ timeout_seconds = (optimizer_timeout_seconds if optimizer_timeout_seconds is not None else timeout_seconds ),
277303 max_tokens = optimizer_max_tokens if optimizer_max_tokens is not None else max_tokens ,
278- enable_thinking = (
279- optimizer_enable_thinking
280- if optimizer_enable_thinking is not None
281- else enable_thinking
304+ enable_thinking = (optimizer_enable_thinking if optimizer_enable_thinking is not None else enable_thinking ),
305+ use_max_completion_tokens = (
306+ optimizer_use_max_completion_tokens
307+ if optimizer_use_max_completion_tokens is not None
308+ else use_max_completion_tokens
282309 ),
283310 )
284311 _update_config (
@@ -287,16 +314,13 @@ def configure_qwen_chat(
287314 base_url = target_base_url if target_base_url is not None else base_url ,
288315 api_key = target_api_key if target_api_key is not None else api_key ,
289316 temperature = target_temperature if target_temperature is not None else temperature ,
290- timeout_seconds = (
291- target_timeout_seconds
292- if target_timeout_seconds is not None
293- else timeout_seconds
294- ),
317+ timeout_seconds = (target_timeout_seconds if target_timeout_seconds is not None else timeout_seconds ),
295318 max_tokens = target_max_tokens if target_max_tokens is not None else max_tokens ,
296- enable_thinking = (
297- target_enable_thinking
298- if target_enable_thinking is not None
299- else enable_thinking
319+ enable_thinking = (target_enable_thinking if target_enable_thinking is not None else enable_thinking ),
320+ use_max_completion_tokens = (
321+ target_use_max_completion_tokens
322+ if target_use_max_completion_tokens is not None
323+ else use_max_completion_tokens
300324 ),
301325 )
302326
@@ -311,6 +335,7 @@ def _update_config(
311335 timeout_seconds : float | str | None = None ,
312336 max_tokens : int | str | None = None ,
313337 enable_thinking : bool | str | None = None ,
338+ use_max_completion_tokens : bool | str | None = None ,
314339) -> None :
315340 env_prefix = role .upper ()
316341 if base_url is not None :
@@ -321,7 +346,7 @@ def _update_config(
321346 os .environ [f"{ env_prefix } _QWEN_CHAT_API_KEY" ] = config .api_key
322347 if temperature is not None :
323348 raw = str (temperature ).strip ()
324- config .temperature = float ( raw ) if raw else None
349+ config .temperature = None if raw . lower () in _OMIT_SENTINELS else float ( raw )
325350 os .environ [f"{ env_prefix } _QWEN_CHAT_TEMPERATURE" ] = raw
326351 if timeout_seconds is not None :
327352 config .timeout_seconds = float (timeout_seconds )
@@ -331,8 +356,11 @@ def _update_config(
331356 os .environ [f"{ env_prefix } _QWEN_CHAT_MAX_TOKENS" ] = str (max_tokens )
332357 if enable_thinking is not None :
333358 config .enable_thinking = _parse_bool (enable_thinking )
334- os .environ [f"{ env_prefix } _QWEN_CHAT_ENABLE_THINKING" ] = (
335- "true" if config .enable_thinking else "false"
359+ os .environ [f"{ env_prefix } _QWEN_CHAT_ENABLE_THINKING" ] = "true" if config .enable_thinking else "false"
360+ if use_max_completion_tokens is not None :
361+ config .use_max_completion_tokens = _parse_bool (use_max_completion_tokens )
362+ os .environ [f"{ env_prefix } _QWEN_CHAT_USE_MAX_COMPLETION_TOKENS" ] = (
363+ "true" if config .use_max_completion_tokens else "false"
336364 )
337365
338366
0 commit comments