| 40 | self._codex_bin = [codex_bin] if isinstance(codex_bin, str) else list(codex_bin) |
| 41 | |
| 42 | def call(self, user_content: str) -> tuple[str, TokenUsage]: |
| 43 | prompt = SYSTEM_PROMPT + "\n\n" + user_content |
| 44 | |
| 45 | cmd = [ |
| 46 | *self._codex_bin, |
| 47 | "exec", |
| 48 | "--json", |
| 49 | "--sandbox", |
| 50 | "read-only", |
| 51 | ] |
| 52 | |
| 53 | cmd.extend(["--model", self._underlying]) |
| 54 | |
| 55 | if self._reasoning_effort: |
| 56 | cmd.extend(["-c", f'model_reasoning_effort="{self._reasoning_effort}"']) |
| 57 | |
| 58 | # Pass prompt on stdin (the "-" argument tells codex to read from stdin). |
| 59 | cmd.append("-") |
| 60 | |
| 61 | result = subprocess.run( |
| 62 | cmd, |
| 63 | input=prompt, |
| 64 | capture_output=True, |
| 65 | text=True, |
| 66 | timeout=CODEX_TIMEOUT_SECONDS, |
| 67 | ) |
| 68 | |
| 69 | content, usage = _parse_jsonl(result.stdout) |
| 70 | |
| 71 | if result.returncode != 0: |
| 72 | detail = result.stderr.strip() or result.stdout.strip() |
| 73 | if "usage limit" in detail.lower(): |
| 74 | raise FatalExtractionError(f"codex usage limit reached: {detail}") |
| 75 | # If codex produced no response content, the failure is |
| 76 | # systemic (auth, network, service outage) rather than |
| 77 | # prompt-specific — abort the entire run instead of failing |
| 78 | # each remaining file one by one. |
| 79 | if not content: |
| 80 | raise FatalExtractionError( |
| 81 | f"codex exec failed with no output (exit {result.returncode}): {detail}" |
| 82 | ) |
| 83 | raise RuntimeError( |
| 84 | f"codex exec failed (exit {result.returncode}): {detail}" |
| 85 | ) |
| 86 | |
| 87 | return content, usage |
| 88 | |
| 89 | @property |
| 90 | def retryable_exceptions(self) -> tuple[type[Exception], ...]: |