Skip to content

Commit fb1a763

Browse files
authored
Merge pull request #29 from LifeIsSoSolong/codex/qwen-chat-optimizer-backend
Support qwen_chat as optimizer backend
2 parents 47063e1 + 41012e2 commit fb1a763

9 files changed

Lines changed: 375 additions & 65 deletions

File tree

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ export QWEN_CHAT_BASE_URL="http://localhost:8000/v1"
105105
export QWEN_CHAT_MODEL="Qwen/Qwen3.5-4B"
106106
```
107107

108+
`qwen_chat` can also be used as the optimizer backend. When optimizer and
109+
target should point to different local vLLM services, use the role-specific
110+
settings:
111+
112+
```bash
113+
python scripts/train.py \
114+
--config configs/searchqa/default.yaml \
115+
--optimizer_backend qwen_chat \
116+
--target_backend qwen_chat \
117+
--optimizer_model Qwen/Qwen3.5-4B \
118+
--target_model Qwen/Qwen3.5-4B \
119+
--optimizer_qwen_chat_base_url http://localhost:8001/v1 \
120+
--target_qwen_chat_base_url http://localhost:8000/v1
121+
```
122+
108123
#### MiniMax
109124

110125
```bash

‎docs/reference/config.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Complete reference for all SkillOpt configuration parameters.
1010
| `model.optimizer` | str | `gpt-5.5` | Optimizer model (for reflection & slow update) |
1111
| `model.target` | str | `gpt-5.5` | Target model (for rollout execution) |
1212
| `model.reasoning_effort` | str | `medium` | Reasoning effort level |
13+
| `model.optimizer_backend` | str | `openai_chat` | Optimizer backend: `openai_chat` / `claude_chat` / `qwen_chat` / `minimax_chat` |
14+
| `model.target_backend` | str | `openai_chat` | Target backend: chat backends plus execution harnesses |
15+
| `model.qwen_chat_base_url` | str | `http://localhost:8000/v1` | Shared Qwen/vLLM OpenAI-compatible endpoint |
16+
| `model.qwen_chat_enable_thinking` | bool | `false` | Shared Qwen thinking flag |
17+
| `model.optimizer_qwen_chat_base_url` | str | — | Optimizer-specific Qwen/vLLM endpoint; overrides shared `qwen_chat_base_url` |
18+
| `model.target_qwen_chat_base_url` | str | — | Target-specific Qwen/vLLM endpoint; overrides shared `qwen_chat_base_url` |
1319

1420
## Training (`train`)
1521

@@ -70,3 +76,10 @@ Complete reference for all SkillOpt configuration parameters.
7076
| `AZURE_OPENAI_API_KEY` / `model.azure_openai_api_key` | Azure API key |
7177
| `OPENAI_API_KEY` | OpenAI API key (for `openai_chat` backend) |
7278
| `ANTHROPIC_API_KEY` | Anthropic API key (for `claude_code_exec` backend) |
79+
| `QWEN_CHAT_BASE_URL` | Shared local vLLM endpoint for `qwen_chat` |
80+
| `QWEN_CHAT_MODEL` | Shared served model name for `qwen_chat` |
81+
| `QWEN_CHAT_API_KEY` | Optional API key for the shared Qwen endpoint |
82+
| `OPTIMIZER_QWEN_CHAT_BASE_URL` | Optimizer-specific local vLLM endpoint |
83+
| `OPTIMIZER_QWEN_CHAT_MODEL` | Optimizer-specific served model name |
84+
| `TARGET_QWEN_CHAT_BASE_URL` | Target-specific local vLLM endpoint |
85+
| `TARGET_QWEN_CHAT_MODEL` | Target-specific served model name |

‎scripts/train.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ def parse_args() -> argparse.Namespace:
173173
p.add_argument("--qwen_chat_timeout_seconds", type=float)
174174
p.add_argument("--qwen_chat_max_tokens", type=int)
175175
p.add_argument("--qwen_chat_enable_thinking", type=_BOOL)
176+
p.add_argument("--optimizer_qwen_chat_base_url", type=str)
177+
p.add_argument("--optimizer_qwen_chat_api_key", type=str)
178+
p.add_argument("--optimizer_qwen_chat_temperature", type=float)
179+
p.add_argument("--optimizer_qwen_chat_timeout_seconds", type=float)
180+
p.add_argument("--optimizer_qwen_chat_max_tokens", type=int)
181+
p.add_argument("--optimizer_qwen_chat_enable_thinking", type=_BOOL)
182+
p.add_argument("--target_qwen_chat_base_url", type=str)
183+
p.add_argument("--target_qwen_chat_api_key", type=str)
184+
p.add_argument("--target_qwen_chat_temperature", type=float)
185+
p.add_argument("--target_qwen_chat_timeout_seconds", type=float)
186+
p.add_argument("--target_qwen_chat_max_tokens", type=int)
187+
p.add_argument("--target_qwen_chat_enable_thinking", type=_BOOL)
176188
p.add_argument("--minimax_base_url", type=str)
177189
p.add_argument("--minimax_api_key", type=str)
178190
p.add_argument("--minimax_model", type=str)
@@ -295,6 +307,18 @@ def parse_args() -> argparse.Namespace:
295307
"qwen_chat_timeout_seconds": "model.qwen_chat_timeout_seconds",
296308
"qwen_chat_max_tokens": "model.qwen_chat_max_tokens",
297309
"qwen_chat_enable_thinking": "model.qwen_chat_enable_thinking",
310+
"optimizer_qwen_chat_base_url": "model.optimizer_qwen_chat_base_url",
311+
"optimizer_qwen_chat_api_key": "model.optimizer_qwen_chat_api_key",
312+
"optimizer_qwen_chat_temperature": "model.optimizer_qwen_chat_temperature",
313+
"optimizer_qwen_chat_timeout_seconds": "model.optimizer_qwen_chat_timeout_seconds",
314+
"optimizer_qwen_chat_max_tokens": "model.optimizer_qwen_chat_max_tokens",
315+
"optimizer_qwen_chat_enable_thinking": "model.optimizer_qwen_chat_enable_thinking",
316+
"target_qwen_chat_base_url": "model.target_qwen_chat_base_url",
317+
"target_qwen_chat_api_key": "model.target_qwen_chat_api_key",
318+
"target_qwen_chat_temperature": "model.target_qwen_chat_temperature",
319+
"target_qwen_chat_timeout_seconds": "model.target_qwen_chat_timeout_seconds",
320+
"target_qwen_chat_max_tokens": "model.target_qwen_chat_max_tokens",
321+
"target_qwen_chat_enable_thinking": "model.target_qwen_chat_enable_thinking",
298322
"minimax_base_url": "model.minimax_base_url",
299323
"minimax_api_key": "model.minimax_api_key",
300324
"minimax_model": "model.minimax_model",
@@ -431,6 +455,12 @@ def _has_model_override(dotted_key: str, legacy_key: str) -> bool:
431455
and not _has_model_override("model.optimizer", "optimizer_model")
432456
):
433457
flat["optimizer_model"] = default_model_for_backend("claude_chat")
458+
if flat.get("optimizer_backend") == "qwen_chat":
459+
if (
460+
str(flat.get("optimizer_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS
461+
and not _has_model_override("model.optimizer", "optimizer_model")
462+
):
463+
flat["optimizer_model"] = default_model_for_backend("qwen_chat")
434464
if flat.get("target_backend") == "claude_chat":
435465
if (
436466
str(flat.get("target_model", "") or "").strip() in _OPENAI_DEFAULT_MODEL_SENTINELS

‎skillopt/config.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@
7979
"model.qwen_chat_timeout_seconds": "qwen_chat_timeout_seconds",
8080
"model.qwen_chat_max_tokens": "qwen_chat_max_tokens",
8181
"model.qwen_chat_enable_thinking": "qwen_chat_enable_thinking",
82+
"model.optimizer_qwen_chat_base_url": "optimizer_qwen_chat_base_url",
83+
"model.optimizer_qwen_chat_api_key": "optimizer_qwen_chat_api_key",
84+
"model.optimizer_qwen_chat_temperature": "optimizer_qwen_chat_temperature",
85+
"model.optimizer_qwen_chat_timeout_seconds": "optimizer_qwen_chat_timeout_seconds",
86+
"model.optimizer_qwen_chat_max_tokens": "optimizer_qwen_chat_max_tokens",
87+
"model.optimizer_qwen_chat_enable_thinking": "optimizer_qwen_chat_enable_thinking",
88+
"model.target_qwen_chat_base_url": "target_qwen_chat_base_url",
89+
"model.target_qwen_chat_api_key": "target_qwen_chat_api_key",
90+
"model.target_qwen_chat_temperature": "target_qwen_chat_temperature",
91+
"model.target_qwen_chat_timeout_seconds": "target_qwen_chat_timeout_seconds",
92+
"model.target_qwen_chat_max_tokens": "target_qwen_chat_max_tokens",
93+
"model.target_qwen_chat_enable_thinking": "target_qwen_chat_enable_thinking",
8294
"model.minimax_base_url": "minimax_base_url",
8395
"model.minimax_api_key": "minimax_api_key",
8496
"model.minimax_model": "minimax_model",

‎skillopt/engine/trainer.py‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -629,14 +629,26 @@ def _build_eval_env(split: str, env_num: int, seed: int):
629629
effort=cfg.get("claude_code_exec_effort", cfg.get("reasoning_effort", "medium")),
630630
max_thinking_tokens=cfg.get("claude_code_exec_max_thinking_tokens", 16384),
631631
)
632-
configure_qwen_chat(
633-
base_url=cfg.get("qwen_chat_base_url") or None,
634-
api_key=cfg.get("qwen_chat_api_key") or None,
635-
temperature=cfg.get("qwen_chat_temperature"),
636-
timeout_seconds=cfg.get("qwen_chat_timeout_seconds"),
637-
max_tokens=cfg.get("qwen_chat_max_tokens"),
638-
enable_thinking=cfg.get("qwen_chat_enable_thinking"),
639-
)
632+
configure_qwen_chat(
633+
base_url=cfg.get("qwen_chat_base_url") or None,
634+
api_key=cfg.get("qwen_chat_api_key") or None,
635+
temperature=cfg.get("qwen_chat_temperature"),
636+
timeout_seconds=cfg.get("qwen_chat_timeout_seconds"),
637+
max_tokens=cfg.get("qwen_chat_max_tokens"),
638+
enable_thinking=cfg.get("qwen_chat_enable_thinking"),
639+
optimizer_base_url=cfg.get("optimizer_qwen_chat_base_url") or None,
640+
optimizer_api_key=cfg.get("optimizer_qwen_chat_api_key") or None,
641+
optimizer_temperature=cfg.get("optimizer_qwen_chat_temperature"),
642+
optimizer_timeout_seconds=cfg.get("optimizer_qwen_chat_timeout_seconds"),
643+
optimizer_max_tokens=cfg.get("optimizer_qwen_chat_max_tokens"),
644+
optimizer_enable_thinking=cfg.get("optimizer_qwen_chat_enable_thinking"),
645+
target_base_url=cfg.get("target_qwen_chat_base_url") or None,
646+
target_api_key=cfg.get("target_qwen_chat_api_key") or None,
647+
target_temperature=cfg.get("target_qwen_chat_temperature"),
648+
target_timeout_seconds=cfg.get("target_qwen_chat_timeout_seconds"),
649+
target_max_tokens=cfg.get("target_qwen_chat_max_tokens"),
650+
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
651+
)
640652
configure_minimax_chat(
641653
base_url=cfg.get("minimax_base_url") or None,
642654
api_key=cfg.get("minimax_api_key") or None,

‎skillopt/model/__init__.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def get_backend_name() -> str:
6464
target = get_target_backend()
6565
if optimizer == "claude_chat" and target == "claude_chat":
6666
return "claude_chat"
67+
if optimizer == "qwen_chat" and target == "qwen_chat":
68+
return "qwen_chat"
6769
if optimizer == "openai_chat" and target == "openai_chat":
6870
return "azure_openai"
6971
if optimizer == "openai_chat" and target == "codex_exec":
@@ -93,6 +95,16 @@ def chat_optimizer(
9395
stage=stage,
9496
timeout=timeout,
9597
)
98+
if get_optimizer_backend() == "qwen_chat":
99+
return _qwen.chat_optimizer(
100+
system=system,
101+
user=user,
102+
max_completion_tokens=max_completion_tokens,
103+
retries=retries,
104+
stage=stage,
105+
reasoning_effort=reasoning_effort,
106+
timeout=timeout,
107+
)
96108
return _openai.chat_optimizer(
97109
system=system,
98110
user=user,
@@ -179,6 +191,18 @@ def chat_optimizer_messages(
179191
return_message=return_message,
180192
timeout=timeout,
181193
)
194+
if get_optimizer_backend() == "qwen_chat":
195+
return _qwen.chat_optimizer_messages(
196+
messages=messages,
197+
max_completion_tokens=max_completion_tokens,
198+
retries=retries,
199+
stage=stage,
200+
reasoning_effort=reasoning_effort,
201+
tools=tools,
202+
tool_choice=tool_choice,
203+
return_message=return_message,
204+
timeout=timeout,
205+
)
182206
return _openai.chat_optimizer_messages(
183207
messages=messages,
184208
max_completion_tokens=max_completion_tokens,
@@ -414,6 +438,18 @@ def configure_qwen_chat(
414438
timeout_seconds: float | str | None = None,
415439
max_tokens: int | str | None = None,
416440
enable_thinking: bool | str | None = None,
441+
optimizer_base_url: str | None = None,
442+
optimizer_api_key: str | None = None,
443+
optimizer_temperature: float | str | None = None,
444+
optimizer_timeout_seconds: float | str | None = None,
445+
optimizer_max_tokens: int | str | None = None,
446+
optimizer_enable_thinking: bool | str | None = None,
447+
target_base_url: str | None = None,
448+
target_api_key: str | None = None,
449+
target_temperature: float | str | None = None,
450+
target_timeout_seconds: float | str | None = None,
451+
target_max_tokens: int | str | None = None,
452+
target_enable_thinking: bool | str | None = None,
417453
) -> None:
418454
_qwen.configure_qwen_chat(
419455
base_url=base_url,
@@ -422,6 +458,18 @@ def configure_qwen_chat(
422458
timeout_seconds=timeout_seconds,
423459
max_tokens=max_tokens,
424460
enable_thinking=enable_thinking,
461+
optimizer_base_url=optimizer_base_url,
462+
optimizer_api_key=optimizer_api_key,
463+
optimizer_temperature=optimizer_temperature,
464+
optimizer_timeout_seconds=optimizer_timeout_seconds,
465+
optimizer_max_tokens=optimizer_max_tokens,
466+
optimizer_enable_thinking=optimizer_enable_thinking,
467+
target_base_url=target_base_url,
468+
target_api_key=target_api_key,
469+
target_temperature=target_temperature,
470+
target_timeout_seconds=target_timeout_seconds,
471+
target_max_tokens=target_max_tokens,
472+
target_enable_thinking=target_enable_thinking,
425473
)
426474

427475

@@ -461,3 +509,4 @@ def set_target_deployment(deployment: str) -> None:
461509
def set_optimizer_deployment(deployment: str) -> None:
462510
_openai.set_optimizer_deployment(deployment)
463511
_claude.set_optimizer_deployment(deployment)
512+
_qwen.set_optimizer_deployment(deployment)

‎skillopt/model/azure_openai.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,10 @@ def get_target_client() -> AzureOpenAI | OpenAI:
336336
from skillopt.model.backend_config import get_target_backend
337337
if get_target_backend() == "qwen_chat":
338338
from skillopt.model import qwen_backend as _qwen
339+
target_config = _qwen.TARGET_CONFIG
339340
_target_client = OpenAI(
340-
base_url=_qwen.BASE_URL,
341-
api_key=_qwen.API_KEY or "dummy",
341+
base_url=target_config.base_url,
342+
api_key=target_config.api_key or "dummy",
342343
)
343344
else:
344345
_target_client = _make_client("target")

‎skillopt/model/backend_config.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ def _parse_int(value: str | None, default: int) -> int:
4949
def set_optimizer_backend(backend: str) -> None:
5050
global OPTIMIZER_BACKEND
5151
OPTIMIZER_BACKEND = normalize_backend_name(backend or "openai_chat")
52-
if OPTIMIZER_BACKEND not in {"openai_chat", "claude_chat", "minimax_chat"}:
52+
if OPTIMIZER_BACKEND not in {"openai_chat", "claude_chat", "qwen_chat", "minimax_chat"}:
5353
raise ValueError(
5454
f"Unsupported optimizer backend: {OPTIMIZER_BACKEND!r}. "
55-
"Supported values are 'openai_chat', 'claude_chat', and 'minimax_chat'."
55+
"Supported values are 'openai_chat', 'claude_chat', 'qwen_chat', and 'minimax_chat'."
5656
)
5757
os.environ["OPTIMIZER_BACKEND"] = OPTIMIZER_BACKEND
5858

@@ -81,7 +81,7 @@ def is_target_exec_backend() -> bool:
8181

8282

8383
def is_optimizer_chat_backend() -> bool:
84-
return OPTIMIZER_BACKEND in {"openai_chat", "claude_chat", "minimax_chat"}
84+
return OPTIMIZER_BACKEND in {"openai_chat", "claude_chat", "qwen_chat", "minimax_chat"}
8585

8686

8787
def is_target_chat_backend() -> bool:

0 commit comments

Comments
 (0)