Skip to content

Commit 3e3df88

Browse files
authored
Merge pull request #112 from TheGameVIX/agent/codex-exec-optimizer-main
fix(codex): support exec optimizer backend
2 parents 0d88397 + 0b94f89 commit 3e3df88

11 files changed

Lines changed: 437 additions & 29 deletions

File tree

‎docs/guideline.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ <h2>Research model backends</h2>
394394
<tr><td><code>claude_chat</code></td><td>Yes</td><td>Yes</td><td>Runs an installed, authenticated Claude Code CLI via <code>claude -p</code>; not a direct Anthropic API client.</td></tr>
395395
<tr><td><code>qwen_chat</code></td><td>Yes</td><td>Yes</td><td>Local or hosted Qwen-compatible server.</td></tr>
396396
<tr><td><code>minimax_chat</code></td><td>Yes</td><td>Yes</td><td>MiniMax chat endpoint.</td></tr>
397-
<tr><td><code>codex_exec</code></td><td>No</td><td>Supported adapters only</td><td>Executes Codex as a target agent.</td></tr>
397+
<tr><td><code>codex_exec</code></td><td>Yes</td><td>Supported adapters only</td><td>Executes Codex for optimizer calls and as a target agent where supported.</td></tr>
398398
<tr><td><code>claude_code_exec</code></td><td>No</td><td>Supported adapters only</td><td>Executes Claude Code as a target agent.</td></tr>
399399
</tbody>
400400
</table>

‎docs/reference/api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ not via a base class subclass. Supported values (as of this writing):
190190
| `qwen_chat` | ✓ | ✓ |
191191
| `minimax_chat` | ✓ | ✓ |
192192
| `openai_compatible` | ✓ | ✓ |
193-
| `codex_exec` | — | ✓ |
193+
| `codex_exec` | ✓ | ✓ |
194194
| `claude_code_exec` | — | ✓ |
195195

196196
See `skillopt/model/backend_config.py` for the live whitelist and

‎docs/reference/config.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ selecting the generic OpenAI-compatible backend.
1616
| `claude_chat` | ✓ | ✓ |
1717
| `qwen_chat` | ✓ | ✓ |
1818
| `minimax_chat` | ✓ | ✓ |
19-
| `codex_exec` | — | ✓ |
19+
| `codex_exec` | ✓ | ✓ |
2020
| `claude_code_exec` | — | ✓ |
2121

2222
MiniMax currently has one shared deployment. `model.minimax_model` is applied
@@ -28,7 +28,7 @@ MiniMax optimizer model and a different target model.
2828
| `model.backend` | str | `azure_openai` | Backward-compatible high-level run label |
2929
| `model.optimizer` | str | `gpt-5.5` | Optimizer deployment/model |
3030
| `model.target` | str | `gpt-5.5` | Target deployment/model |
31-
| `model.optimizer_backend` | str | `openai_chat` | Optimizer client path; chat backends only |
31+
| `model.optimizer_backend` | str | `openai_chat` | Optimizer client path; chat backends plus `codex_exec` |
3232
| `model.target_backend` | str | `openai_chat` | Target client path; chat or exec backend |
3333
| `model.reasoning_effort` | str | `medium` | Shared reasoning effort |
3434
| `model.rewrite_reasoning_effort` | str | empty | Optional full-rewrite effort override |

‎scripts/eval_only.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,10 @@ def _has_model_override(dotted_key: str, legacy_key: str) -> bool:
320320
cfg.setdefault("optimizer_backend", "claude_chat")
321321
cfg.setdefault("target_backend", "claude_chat")
322322
elif backend in {"codex", "codex_exec"}:
323-
cfg.setdefault("optimizer_backend", "openai_chat")
324-
cfg.setdefault("target_backend", "codex_exec")
323+
if not _has_model_override("model.optimizer_backend", "optimizer_backend"):
324+
cfg["optimizer_backend"] = "codex_exec"
325+
if not _has_model_override("model.target_backend", "target_backend"):
326+
cfg["target_backend"] = "codex_exec"
325327
elif backend == "claude_code_exec":
326328
cfg.setdefault("optimizer_backend", "openai_chat")
327329
cfg.setdefault("target_backend", "claude_code_exec")

‎scripts/train.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,10 @@ def _has_model_override(dotted_key: str, legacy_key: str) -> bool:
438438
flat.setdefault("optimizer_backend", "claude_chat")
439439
flat.setdefault("target_backend", "claude_chat")
440440
elif backend in {"codex", "codex_exec"}:
441-
flat.setdefault("optimizer_backend", "openai_chat")
442-
flat.setdefault("target_backend", "codex_exec")
441+
if not _has_model_override("model.optimizer_backend", "optimizer_backend"):
442+
flat["optimizer_backend"] = "codex_exec"
443+
if not _has_model_override("model.target_backend", "target_backend"):
444+
flat["target_backend"] = "codex_exec"
443445
elif backend == "claude_code_exec":
444446
flat.setdefault("optimizer_backend", "openai_chat")
445447
flat.setdefault("target_backend", "claude_code_exec")

‎skillopt/engine/trainer.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -670,12 +670,14 @@ def _build_eval_env(split: str, env_num: int, seed: int):
670670
optimizer_backend = cfg.get("optimizer_backend")
671671
target_backend = cfg.get("target_backend")
672672
if not optimizer_backend or not target_backend:
673-
if backend in {"claude", "claude_chat"}:
674-
optimizer_backend = optimizer_backend or "claude_chat"
675-
target_backend = target_backend or "claude_chat"
676-
elif backend in {"codex", "codex_exec"}:
677-
optimizer_backend = optimizer_backend or "openai_chat"
678-
target_backend = target_backend or "codex_exec"
673+
if backend in {"claude", "claude_chat"}:
674+
optimizer_backend = optimizer_backend or "claude_chat"
675+
target_backend = target_backend or "claude_chat"
676+
elif backend in {"codex", "codex_exec"}:
677+
if optimizer_backend in (None, "", "openai_chat"):
678+
optimizer_backend = "codex_exec"
679+
if target_backend in (None, "", "openai_chat"):
680+
target_backend = "codex_exec"
679681
elif backend == "claude_code_exec":
680682
optimizer_backend = optimizer_backend or "openai_chat"
681683
target_backend = target_backend or "claude_code_exec"

‎skillopt/envs/_template/config_template.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@ evaluation:
5050
# ── Model ────────────────────────────────────────
5151
# Override only what differs from the inherited defaults.
5252
model:
53-
optimizer_backend: openai_chat # openai_chat | claude_chat | qwen_chat | minimax_chat
54-
target_backend: openai_chat # … plus codex_exec / claude_code_exec for target only
53+
optimizer_backend: openai_chat # openai_chat | claude_chat | qwen_chat | minimax_chat | codex_exec
54+
target_backend: openai_chat # chat backends plus codex_exec / claude_code_exec
5555
reasoning_effort: medium

‎skillopt/model/__init__.py‎

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from skillopt.model import azure_openai as _openai
88
from skillopt.model import claude_backend as _claude
9+
from skillopt.model import codex_backend as _codex
910
from skillopt.model import minimax_backend as _minimax
1011
from skillopt.model import openai_compatible_backend as _openai_compat
1112
from skillopt.model import qwen_backend as _qwen
@@ -41,10 +42,14 @@ def set_backend(name: str | None) -> str:
4142
set_target_backend("claude_chat")
4243
return "claude_chat"
4344
if normalized == "codex":
44-
set_optimizer_backend("openai_chat")
45+
set_optimizer_backend("codex_exec")
4546
set_target_backend("codex_exec")
4647
return "codex"
47-
if normalized in {"codex_exec", "claude_code_exec"}:
48+
if normalized == "codex_exec":
49+
set_optimizer_backend("codex_exec")
50+
set_target_backend("codex_exec")
51+
return normalized
52+
if normalized == "claude_code_exec":
4853
set_optimizer_backend("openai_chat")
4954
set_target_backend(normalized)
5055
return normalized
@@ -73,7 +78,7 @@ def get_backend_name() -> str:
7378
return "qwen_chat"
7479
if optimizer == "openai_chat" and target == "openai_chat":
7580
return "azure_openai"
76-
if optimizer == "openai_chat" and target == "codex_exec":
81+
if optimizer == "codex_exec" and target == "codex_exec":
7782
return "codex"
7883
if optimizer == "openai_chat" and target == "qwen_chat":
7984
return "qwen_chat"
@@ -132,6 +137,15 @@ def chat_optimizer(
132137
reasoning_effort=reasoning_effort,
133138
timeout=timeout,
134139
)
140+
if get_optimizer_backend() == "codex_exec":
141+
return _codex.chat_optimizer(
142+
system=system,
143+
user=user,
144+
max_completion_tokens=max_completion_tokens,
145+
retries=retries,
146+
stage=stage,
147+
timeout=timeout,
148+
)
135149
return _openai.chat_optimizer(
136150
system=system,
137151
user=user,
@@ -265,6 +279,17 @@ def chat_optimizer_messages(
265279
return_message=return_message,
266280
timeout=timeout,
267281
)
282+
if get_optimizer_backend() == "codex_exec":
283+
return _codex.chat_optimizer_messages(
284+
messages=messages,
285+
max_completion_tokens=max_completion_tokens,
286+
retries=retries,
287+
stage=stage,
288+
tools=tools,
289+
tool_choice=tool_choice,
290+
return_message=return_message,
291+
timeout=timeout,
292+
)
268293
return _openai.chat_optimizer_messages(
269294
messages=messages,
270295
max_completion_tokens=max_completion_tokens,
@@ -449,6 +474,17 @@ def get_token_summary() -> dict:
449474
summary[stage]["prompt_tokens"] += values["prompt_tokens"]
450475
summary[stage]["completion_tokens"] += values["completion_tokens"]
451476
summary[stage]["total_tokens"] += values["total_tokens"]
477+
codex_summary = _codex.get_token_summary()
478+
for stage, values in codex_summary.items():
479+
if stage == "_total":
480+
continue
481+
if stage not in summary:
482+
summary[stage] = values
483+
continue
484+
summary[stage]["calls"] += values["calls"]
485+
summary[stage]["prompt_tokens"] += values["prompt_tokens"]
486+
summary[stage]["completion_tokens"] += values["completion_tokens"]
487+
summary[stage]["total_tokens"] += values["total_tokens"]
452488
total = {
453489
"calls": 0,
454490
"prompt_tokens": 0,
@@ -472,6 +508,7 @@ def reset_token_tracker() -> None:
472508
_qwen.reset_token_tracker()
473509
_minimax.reset_token_tracker()
474510
_openai_compat.reset_token_tracker()
511+
_codex.reset_token_tracker()
475512

476513

477514
def configure_azure_openai(
@@ -622,6 +659,7 @@ def set_reasoning_effort(effort: str | None) -> None:
622659
_qwen.set_reasoning_effort(effort)
623660
_minimax.set_reasoning_effort(effort)
624661
_openai_compat.set_reasoning_effort(effort)
662+
_codex.set_reasoning_effort(effort)
625663

626664

627665
def set_target_deployment(deployment: str) -> None:
@@ -630,10 +668,12 @@ def set_target_deployment(deployment: str) -> None:
630668
_qwen.set_target_deployment(deployment)
631669
_minimax.set_target_deployment(deployment)
632670
_openai_compat.set_target_deployment(deployment)
671+
_codex.set_target_deployment(deployment)
633672

634673

635674
def set_optimizer_deployment(deployment: str) -> None:
636675
_openai.set_optimizer_deployment(deployment)
637676
_claude.set_optimizer_deployment(deployment)
638677
_qwen.set_optimizer_deployment(deployment)
639678
_openai_compat.set_optimizer_deployment(deployment)
679+
_codex.set_optimizer_deployment(deployment)

‎skillopt/model/backend_config.py‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,18 @@ 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", "qwen_chat", "minimax_chat", "openai_compatible"}:
52+
if OPTIMIZER_BACKEND not in {
53+
"openai_chat",
54+
"claude_chat",
55+
"qwen_chat",
56+
"minimax_chat",
57+
"openai_compatible",
58+
"codex_exec",
59+
}:
5360
raise ValueError(
5461
f"Unsupported optimizer backend: {OPTIMIZER_BACKEND!r}. "
55-
"Supported values are 'openai_chat', 'claude_chat', 'qwen_chat', 'minimax_chat', and 'openai_compatible'."
62+
"Supported values are 'openai_chat', 'claude_chat', 'qwen_chat', 'minimax_chat', "
63+
"'openai_compatible', and 'codex_exec'."
5664
)
5765
os.environ["OPTIMIZER_BACKEND"] = OPTIMIZER_BACKEND
5866

@@ -81,7 +89,14 @@ def is_target_exec_backend() -> bool:
8189

8290

8391
def is_optimizer_chat_backend() -> bool:
84-
return OPTIMIZER_BACKEND in {"openai_chat", "claude_chat", "qwen_chat", "minimax_chat", "openai_compatible"}
92+
return OPTIMIZER_BACKEND in {
93+
"openai_chat",
94+
"claude_chat",
95+
"qwen_chat",
96+
"minimax_chat",
97+
"openai_compatible",
98+
"codex_exec",
99+
}
85100

86101

87102
def is_target_chat_backend() -> bool:

‎skillopt/model/codex_backend.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
CompatToolFunction,
1919
tracker,
2020
)
21+
from skillopt.model.backend_config import get_codex_exec_config
2122

2223

2324
CODEX_BIN = os.environ.get("CODEX_CLI_BIN", "codex")
@@ -286,20 +287,21 @@ def _run_codex_exec(
286287
timeout: int | None,
287288
) -> tuple[str, dict[str, int]]:
288289
with tempfile.TemporaryDirectory(prefix="skillopt_codex_") as temp_dir:
290+
config = get_codex_exec_config()
289291
output_path = os.path.join(temp_dir, "last_message.txt")
290292
image_paths = _materialize_attachments(attachments, temp_dir)
293+
profile = str(config.get("profile") or os.environ.get("CODEX_PROFILE", "")).strip()
294+
reasoning_effort = str(REASONING_EFFORT or config.get("reasoning_effort") or "").strip()
291295

292296
command = [
293-
CODEX_BIN,
297+
str(config.get("path") or CODEX_BIN),
294298
"exec",
295299
"--json",
296300
"--ephemeral",
297-
"--profile",
298-
CODEX_PROFILE,
299301
"-c",
300-
"approval_policy=\"never\"",
302+
f"approval_policy={json.dumps(str(config.get('approval_policy') or 'never'))}",
301303
"--sandbox",
302-
CODEX_SANDBOX_MODE,
304+
str(config.get("sandbox") or CODEX_SANDBOX_MODE),
303305
"--skip-git-repo-check",
304306
"--cd",
305307
_default_working_directory(),
@@ -309,8 +311,11 @@ def _run_codex_exec(
309311
output_path,
310312
]
311313

312-
if REASONING_EFFORT:
313-
command.extend(["-c", f"model_reasoning_effort={json.dumps(REASONING_EFFORT)}"])
314+
if profile:
315+
command.extend(["--profile", profile])
316+
317+
if reasoning_effort and reasoning_effort != "none":
318+
command.extend(["-c", f"model_reasoning_effort={json.dumps(reasoning_effort)}"])
314319

315320
schema_path = None
316321
if output_schema is not None:

0 commit comments

Comments
 (0)