Repository navigation
feat: add Codex agent adapter #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,10 +45,12 @@ reverse-api-engineer show <run_id> --json | |
| reverse-api-engineer agent -p "<goal>" -u "<start url>" --dry-run | ||
| ``` | ||
|
|
||
| This validates the prompt, URL, config and environment without opening a browser. Fix every `error` check before running. A `warn` on the API key is fine when the SDK authenticates another way (for example a logged-in Claude Code). | ||
| This validates the prompt, URL, config and environment without opening a browser. Fix every `error` check before running. A `warn` on the API key is fine when the SDK authenticates another way (for example a logged-in Claude Code or Codex CLI). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The Codex example doesn't fit this sentence. Codex never gets an API-key Prompt for AI agents-This validates the prompt, URL, config and environment without opening a browser. Fix every
|
||
|
|
||
| `would_run.agent_provider` in the output is the browser the run will use. `chrome-mcp` means the user's own Chrome with their logins; `auto` and `agent-browser` start a fresh browser without them. When the task needs the user's login and the provider is not `chrome-mcp`, ask before switching it (see [sessions](references/sessions.md)). | ||
|
|
||
| The `codex` SDK reuses the local Codex CLI login. In the current RAE adapter, Codex agent capture requires `agent_provider: "auto"` so Playwright MCP records the HAR; it does not support `chrome-mcp` or `agent-browser` capture. `agent --dry-run` reports Codex login and browser-provider checks before opening a browser. | ||
|
|
||
| For `chrome-mcp`, the `chrome-mcp:auto-connect` check covers the connection to the user's Chrome: | ||
|
|
||
| - `ok`: Chrome is reachable. The user still has to accept Chrome's connection prompt when the run starts. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,8 +17,9 @@ rae calls a model through the SDK selected in its config: | |||||
| | `opencode` | An OpenCode server rae starts or reuses. Defaults to a free model; can route to local models through Ollama | | ||||||
| | `copilot` | GitHub Copilot credentials (`GITHUB_COPILOT_TOKEN`) | | ||||||
| | `cursor` | `CURSOR_API_KEY` | | ||||||
| | `codex` | Existing local Codex CLI login; Codex Desktop's bundled CLI is preferred when present, then `codex` on `PATH` | | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The Desktop-bundled CLI preference only exists on Windows ( Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| `agent --dry-run` reports which of these it can see. A missing env var is a warning, not an error, because some SDKs find credentials on their own. | ||||||
| `agent --dry-run` reports which of these it can see. A missing env var is a warning, not an error, because some SDKs find credentials on their own. Codex defaults to `gpt-6-luna` unless `codex_model` or `--model` overrides it. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Prompt for AI agents |
||||||
|
|
||||||
| ## Settings | ||||||
|
|
||||||
|
|
@@ -28,8 +29,9 @@ Settings live in `~/.reverse-api/config.json`. Only two have per-run flags on `a | |||||
| |---|---| | ||||||
| | `agent_provider` | `chrome-mcp` (the user's real Chrome, no HAR; default for new configs in recent versions), `auto` (Playwright MCP, records a HAR; the default in older versions), `agent-browser` (Vercel agent-browser CLI, records a HAR) | | ||||||
| | `output_language` | `python`, `javascript`, `typescript`, `go`, `java`, `csharp`, `php`, `ruby`, `c`, `powershell` | | ||||||
| | `sdk` | `claude`, `opencode`, `copilot`, `cursor` | | ||||||
| | `sdk` | `claude`, `opencode`, `copilot`, `cursor`, `codex` | | ||||||
| | `claude_code_model` | Model for the `claude` SDK; `agent -m` overrides it per run | | ||||||
| | `codex_model` | Optional model for the `codex` SDK; defaults to `gpt-6-luna` | | ||||||
| | `output_dir` | Where runs are stored; `null` means `~/.reverse-api/runs` | | ||||||
|
|
||||||
| The user can also change them interactively by running `reverse-api-engineer` and typing `/settings`. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,6 +130,8 @@ def default_model_for_configured_sdk(sdk: str | None = None) -> str: | |
| return str(config_manager.get("copilot_model") or "gpt-5") | ||
| if s == "cursor": | ||
| return str(config_manager.get("cursor_model") or "composer-2.5") | ||
| if s == "codex": | ||
| return str(config_manager.get("codex_model") or "gpt-6-luna") | ||
| return str(config_manager.get("claude_code_model") or "claude-sonnet-4-6") | ||
|
|
||
|
|
||
|
|
@@ -453,11 +455,26 @@ def _build_dry_run_payload( | |
| "copilot": "GITHUB_COPILOT_TOKEN", | ||
| "cursor": "CURSOR_API_KEY", | ||
| }.get(sdk) | ||
| if sdk_env_var is None: | ||
| if sdk == "codex": | ||
| from .codex_engineer import codex_login_status | ||
|
|
||
| codex_ok, codex_message = codex_login_status() | ||
| checks.append({ | ||
| "name": "sdk:codex", | ||
| "status": "ok" if codex_ok else "error", | ||
| "message": codex_message, | ||
| }) | ||
| if agent_provider != "auto": | ||
| checks.append({ | ||
| "name": "sdk:codex:browser", | ||
| "status": "error", | ||
| "message": "Codex agent capture requires agent_provider='auto' to save a Playwright HAR", | ||
| }) | ||
| elif sdk_env_var is None: | ||
| checks.append({ | ||
| "name": "sdk", | ||
| "status": "error", | ||
| "message": f"unknown sdk {sdk!r}; expected 'claude', 'opencode', 'copilot', or 'cursor'", | ||
| "message": f"unknown sdk {sdk!r}; expected 'claude', 'opencode', 'copilot', 'cursor', or 'codex'", | ||
| }) | ||
| elif not os.environ.get(sdk_env_var): | ||
| checks.append({ | ||
|
|
@@ -582,14 +599,16 @@ def _build_dry_run_payload( | |
| "opencode": "opencode_model", | ||
| "copilot": "copilot_model", | ||
| "cursor": "cursor_model", | ||
| "codex": "codex_model", | ||
| }.get(sdk, "claude_code_model") | ||
| sdk_default_model = { | ||
| "claude": "claude-sonnet-4-6", | ||
| "opencode": "claude-opus-4-6", | ||
| "copilot": "gpt-5", | ||
| "cursor": "composer-2.5", | ||
| "codex": "gpt-6-luna", | ||
| }.get(sdk, "claude-sonnet-4-6") | ||
| resolved_model = model or config_manager.get(sdk_model_key, sdk_default_model) | ||
| resolved_model = model or config_manager.get(sdk_model_key) or sdk_default_model | ||
|
|
||
| return { | ||
| "schema_version": AGENT_JSON_SCHEMA_VERSION, | ||
|
|
@@ -855,6 +874,7 @@ def _handle_settings_action(mode_color: str = THEME_PRIMARY) -> bool: # noqa: C | |
| choices = [ | ||
| Choice(title="Agent Provider", value="agent_provider"), | ||
| Choice(title="Claude Code Model", value="claude_code_model"), | ||
| Choice(title="Codex Model", value="codex_model"), | ||
| Choice(title="Copilot Model", value="copilot_model"), | ||
| Choice(title="Cursor Model", value="cursor_model"), | ||
| Choice(title="Cursor Web Search", value="cursor_web_search"), | ||
|
|
@@ -906,6 +926,7 @@ def _handle_settings_action(mode_color: str = THEME_PRIMARY) -> bool: # noqa: C | |
| elif action == "sdk": | ||
| sdk_choices = [ | ||
| Choice(title="claude", value="claude"), | ||
| Choice(title="codex", value="codex"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Choosing codex in /settings leaves Prompt for AI agents |
||
| Choice(title="copilot", value="copilot"), | ||
| Choice(title="cursor", value="cursor"), | ||
| Choice(title="opencode", value="opencode"), | ||
|
|
@@ -927,6 +948,17 @@ def _handle_settings_action(mode_color: str = THEME_PRIMARY) -> bool: # noqa: C | |
| config_manager.set("sdk", sdk) | ||
| console.print(f" [dim]updated[/dim] sdk: {sdk}\n") | ||
|
|
||
| elif action == "codex_model": | ||
| current = str(config_manager.get("codex_model") or "") | ||
| model = questionary.text( | ||
| "Codex model (blank uses GPT-6 Luna)", | ||
| default=current, | ||
| ).ask() | ||
| if model is not None: | ||
| selected = model.strip() or None | ||
| config_manager.set("codex_model", selected) | ||
| console.print(f" [dim]updated[/dim] codex model: {selected or 'gpt-6-luna'}\n") | ||
|
|
||
| elif action == "output_language": | ||
| lang_choices = [ | ||
| Choice(title="python", value="python"), | ||
|
|
@@ -1524,7 +1556,7 @@ def _session_browser_note(agent_provider: str, headless: bool) -> str | None: | |
| return None | ||
|
|
||
|
|
||
| def run_auto_capture( | ||
| def run_auto_capture( # noqa: C901 - SDK/provider/session lifecycle dispatch | ||
| prompt: str | None = None, | ||
| url: str | None = None, | ||
| model: str | None = None, | ||
|
|
@@ -1619,16 +1651,32 @@ def run_auto_capture( | |
| url=url, | ||
| model=model, | ||
| mode=mode_label, | ||
| sdk=sdk, | ||
| paths={"har_dir": str(get_har_dir(run_id, output_dir))}, | ||
| ) | ||
|
|
||
| try: | ||
| output_language = config_manager.get("output_language", "python") | ||
| from .auto_engineer import ClaudeAutoEngineer, CopilotAutoEngineer, OpenCodeAutoEngineer | ||
| from .codex_engineer import CodexAutoEngineer | ||
| from .cursor_engineer import CursorAutoEngineer | ||
|
|
||
| engineer: ClaudeAutoEngineer | CopilotAutoEngineer | OpenCodeAutoEngineer | CursorAutoEngineer | ||
| if sdk == "opencode": | ||
| engineer: ClaudeAutoEngineer | CopilotAutoEngineer | OpenCodeAutoEngineer | CursorAutoEngineer | CodexAutoEngineer | ||
| if sdk == "codex": | ||
| engineer = CodexAutoEngineer( | ||
| run_id=run_id, | ||
| prompt=prompt, | ||
| codex_model=model or config_manager.get("codex_model", "default"), | ||
| output_dir=output_dir, | ||
| agent_provider=agent_provider, | ||
| enable_sync=config_manager.get("real_time_sync", False), | ||
| sdk=sdk, | ||
| output_language=output_language, | ||
| interactive=interactive, | ||
| headless=headless, | ||
| start_url=url, | ||
| ) | ||
| elif sdk == "opencode": | ||
| from .auto_engineer import OpenCodeAutoEngineer | ||
|
|
||
| engineer = OpenCodeAutoEngineer( | ||
|
|
@@ -1704,7 +1752,10 @@ def run_auto_capture( | |
| if resume: | ||
| core.resume_session, core.cost_before = _resume_point(resume) | ||
| # A new browser stretch records its own HAR instead of replacing the run's capture. | ||
| engineer.mcp_run_id = f"{run_id}-{int(time.time())}" | ||
| if hasattr(engineer, "mcp_run_id"): | ||
| engineer.mcp_run_id = f"{run_id}-{int(time.time())}" | ||
| if sdk == "codex": | ||
| engineer.har_path = get_har_dir(engineer.mcp_run_id, output_dir) / "recording.har" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Resumed Codex captures are written under a timestamped HAR directory but that path is not persisted or used by the JSON payload. Preserve the returned HAR path in run history and have payload construction use it. Prompt for AI agents |
||
|
|
||
| if json_event_sink is not None: | ||
| from .json_stream import attach_json_stream_to_engineer | ||
|
|
@@ -1755,6 +1806,15 @@ def run_auto_capture( | |
| "mode": mode_label, | ||
| "script_path": (result or {}).get("script_path"), | ||
| "usage": (result or {}).get("usage", {}), | ||
| **( | ||
| { | ||
| "har_path": (result or {}).get("har_path"), | ||
| "har_entries": (result or {}).get("har_entries", 0), | ||
| "mcp_tool_calls": (result or {}).get("mcp_tool_calls", []), | ||
| } | ||
| if sdk == "codex" | ||
| else {} | ||
| ), | ||
| **( | ||
| {"error": "interrupted"} | ||
| if interrupted | ||
|
|
@@ -2032,6 +2092,7 @@ def run_engineer( | |
| "opencode": resume_model or config_manager.get("opencode_model", DEFAULT_OPENCODE_MODEL), | ||
| "copilot": resume_model or config_manager.get("copilot_model", "gpt-5"), | ||
| "cursor": model or config_manager.get("cursor_model", "composer-2.5"), | ||
| "codex": resume_model or model or config_manager.get("codex_model") or "gpt-6-luna", | ||
| }.get(sdk) or model or config_manager.get("claude_code_model", "claude-sonnet-4-6") | ||
| on_session = _session_recorder(run_id, "engineer", sdk, session_model, output_dir, output_mode) | ||
|
|
||
|
|
@@ -2101,6 +2162,26 @@ def run_engineer( | |
| cost_before=cost_before, | ||
| on_session=on_session, | ||
| ) | ||
| elif sdk == "codex": | ||
| result = run_reverse_engineering( | ||
| run_id=run_id, | ||
| har_path=har_path, | ||
| prompt=prompt, | ||
| model=session_model, | ||
| codex_model=session_model, | ||
| output_dir=output_dir, | ||
| sdk=sdk, | ||
| enable_sync=enable_sync, | ||
| additional_instructions=additional_instructions, | ||
| is_fresh=is_fresh, | ||
| output_language=output_language, | ||
| output_mode=output_mode, | ||
| interactive=interactive, | ||
| json_event_sink=json_event_sink, | ||
| resume_session=resume_session, | ||
| cost_before=cost_before, | ||
| on_session=on_session, | ||
| ) | ||
| else: | ||
| result = run_reverse_engineering( | ||
| run_id=run_id, | ||
|
|
@@ -2456,8 +2537,12 @@ def _show_error_payload(message: str, rid: str | None = None) -> dict: | |
| usage = run.get("usage", {}) | ||
| details["input_tokens"] = usage.get("input_tokens", 0) | ||
| details["output_tokens"] = usage.get("output_tokens", 0) | ||
| details["cache_creation_input_tokens"] = usage.get("cache_creation_input_tokens", 0) | ||
| details["cache_read_input_tokens"] = usage.get("cache_read_input_tokens", 0) | ||
| details["cache_creation_input_tokens"] = usage.get( | ||
| "cache_creation_input_tokens", usage.get("cache_write_tokens", usage.get("cache_creation_tokens", 0)) | ||
| ) | ||
| details["cache_read_input_tokens"] = usage.get( | ||
| "cache_read_input_tokens", usage.get("cache_read_tokens", 0) | ||
| ) | ||
|
|
||
| # Artifact paths with existence | ||
| har_dir = base_dir / "har" / rid | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The bundled Codex CLI preference is implemented only on Windows, but this documentation presents it as platform-independent. Qualify the preference as Windows-only, or implement bundled-CLI discovery on the other supported platforms.
Prompt for AI agents