@@ -417,6 +417,8 @@ def _max_tokens(level: int) -> int:
417417def _provider (model : str ) -> str :
418418 if model == "claude-cli" :
419419 return "claude-cli"
420+ if model == "codex-cli" :
421+ return "codex-cli"
420422 if model .startswith ("gemini" ):
421423 return "gemini"
422424 if model .startswith ("claude" ):
@@ -535,7 +537,8 @@ def _translate(text: str, lang: str) -> str:
535537 f"时使用同一个 key。\n \n "
536538 f"---\n \n { text } "
537539 )
538- return _call (TRANSLATE_MODEL , system , prompt , len (text ) * 3 )
540+ _tmodel = NOTE_MODEL if NOTE_MODEL in ("claude-cli" , "codex-cli" ) else TRANSLATE_MODEL
541+ return _call (_tmodel , system , prompt , len (text ) * 3 )
539542
540543
541544_MODEL_MAX_COMPLETION = {
@@ -588,6 +591,48 @@ def _call(model: str, system: str, user: str, max_tokens: int,
588591 raise RuntimeError (f"claude -p failed (code { result .returncode } ): { result .stderr [:500 ]} " )
589592 return content
590593
594+ # ── Codex CLI mode: call `codex exec` as subprocess ──────────────────
595+ # Auth is handled by the `codex` CLI itself (prior `codex login`).
596+ # We run non-interactively, read-only sandbox, outside a git repo, and
597+ # capture only the agent's final message via `-o <file>` so we don't
598+ # have to parse the streaming event log on stdout.
599+ if _provider (model ) == "codex-cli" :
600+ import subprocess as _sp
601+ import tempfile as _tf
602+ import os as _os2
603+ out_fd , out_file = _tf .mkstemp (prefix = "codex_out_" , suffix = ".txt" )
604+ _os2 .close (out_fd )
605+ try :
606+ prompt_text = f"{ system } \n \n { user } " if system else user
607+ cmd = [
608+ "codex" , "exec" ,
609+ "--skip-git-repo-check" ,
610+ "-s" , "read-only" ,
611+ "-o" , out_file ,
612+ "-" , # read prompt from stdin
613+ ]
614+ result = _sp .run (
615+ cmd , input = prompt_text , capture_output = True , text = True ,
616+ timeout = 1800 ,
617+ )
618+ try :
619+ content = Path (out_file ).read_text (encoding = "utf-8" ).strip ()
620+ except Exception :
621+ content = ""
622+ if _truncated is not None :
623+ _truncated .append (False ) # CLI handles its own limits
624+ if result .returncode != 0 and not content :
625+ err = (result .stderr or result .stdout or "" ).strip ()
626+ raise RuntimeError (
627+ f"codex exec failed (code { result .returncode } ): { err [:500 ]} "
628+ )
629+ return content
630+ finally :
631+ try :
632+ Path (out_file ).unlink ()
633+ except Exception :
634+ pass
635+
591636 client = _get_client_for (model )
592637
593638 if _provider (model ) == "anthropic" :
@@ -996,7 +1041,7 @@ def generate_section(
9961041 terms .add (t )
9971042 term_list = ", " .join (sorted (terms )[:30 ])
9981043 v_user = _P ("verify" ).format (term_list = term_list , draft = draft [:2500 ])
999- _vmodel = NOTE_MODEL if NOTE_MODEL == "claude-cli" else VERIFY_MODEL
1044+ _vmodel = NOTE_MODEL if NOTE_MODEL in ( "claude-cli" , "codex-cli" ) else VERIFY_MODEL
10001045 v_result = _call (_vmodel , "" , v_user , 1500 )
10011046 if not v_result .strip ().upper ().startswith ("APPROVED" ):
10021047 if len (v_result ) > len (draft ) * 0.3 :
@@ -2138,7 +2183,7 @@ def main() -> None:
21382183 parser .add_argument ("--score" , action = "store_true" ,
21392184 help = "Enable self-scoring (dev mode: prints coverage/terminology scores)" )
21402185 parser .add_argument ("--model" , metavar = "MODEL" , default = None ,
2141- help = "Override NOTE_MODEL (e.g. claude-cli, gemini-2.5-flash)" )
2186+ help = "Override NOTE_MODEL (e.g. codex-cli, claude-cli, gemini-2.5-flash)" )
21422187 args = parser .parse_args ()
21432188
21442189 if args .score :
0 commit comments