diff --git a/plugins/pr-review/README.md b/plugins/pr-review/README.md
index 47f60f3f..437beb17 100644
--- a/plugins/pr-review/README.md
+++ b/plugins/pr-review/README.md
@@ -214,6 +214,8 @@ PR reviews are automatically triggered when:
| `acp-command` | Yes for `acp` mode | `''` | Command used to start the ACP server. The command must already be available in the runner environment or be runnable through a package manager. Examples: `npx -y @zed-industries/codex-acp@0.12.0`, `codex-acp`, `claude-agent-acp`, `npx -y @agentclientprotocol/claude-agent-acp`. |
| `acp-prompt-timeout` | No | `'1800'` | Timeout in seconds for one ACP prompt turn |
| `llm-base-url` | No | `''` | Custom LLM endpoint URL |
+| `llm-extra-body` | No | `''` | JSON object of extra request-body fields forwarded to the LLM provider, e.g. `{"enable_thinking": false}` to turn off default thinking on Qwen models. Ignored in ACP mode. |
+| `max-iterations` | No | `''` | Cap on agent iterations (model call + tool action) per review run. Bounds runaway exploration; empty uses the SDK default. Ignored in ACP mode. |
| `review-style` | No | `roasted` | **[DEPRECATED]** Previously chose between `standard` and `roasted` review styles. Now ignored — the styles have been merged into a single unified skill. |
| `require-evidence` | No | `'false'` | Require the reviewer to enforce an `Evidence` section in the PR description with end-to-end proof: screenshots/videos for frontend work, commands and runtime output for backend or scripts, and an agent conversation link when applicable. Test output alone does not qualify. |
| `use-sub-agents` | No | `'false'` | Enable sub-agent delegation for file-level reviews in `openhands` mode. The main agent acts as a coordinator that delegates per-file review work to `file_reviewer` sub-agents via the SDK TaskToolSet, then consolidates findings into a single PR review. Useful for large PRs with many changed files. **Disabled by default** due to high token costs and potential timeouts (see [#208](https://github.com/OpenHands/extensions/issues/208)). Set to `'true'` to opt in. Ignored in ACP mode. |
diff --git a/plugins/pr-review/action.yml b/plugins/pr-review/action.yml
index e0c769e9..57a1cca5 100644
--- a/plugins/pr-review/action.yml
+++ b/plugins/pr-review/action.yml
@@ -41,6 +41,22 @@ inputs:
description: LLM base URL (optional, for custom LLM endpoints)
required: false
default: ''
+ llm-extra-body:
+ description: >
+ JSON object of extra request-body fields forwarded to the LLM
+ provider (optional). Use it for provider-specific switches such as
+ '{"enable_thinking": false}' or '{"thinking_budget": 2048}' on
+ models that think by default. Ignored in ACP mode.
+ required: false
+ default: ''
+ max-iterations:
+ description: >
+ Maximum agent iterations (model call + tool action) for the review
+ run (optional). Bounds runaway exploration so the job ends with a
+ clear error instead of hitting the workflow timeout. Empty uses the
+ SDK default. Ignored in ACP mode.
+ required: false
+ default: ''
review-style:
description: "[DEPRECATED] Previously chose between 'standard' and 'roasted' review styles. These have been merged into a single code-review skill. The input is kept for backward compatibility but no longer changes behavior. Will be removed in a future version."
required: false
@@ -130,6 +146,14 @@ runs:
path: pr-repo
submodules: recursive
+ # fetch-depth 0 brings the base branch in as origin/ only. The reviewer's
+ # natural `git diff ` otherwise fails with "unknown revision" and burns steps.
+ - name: Create local base branch
+ shell: bash
+ env:
+ BASE_REF: ${{ github.event.pull_request.base.ref }}
+ run: git -C pr-repo branch "$BASE_REF" "origin/$BASE_REF" || true
+
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -258,6 +282,8 @@ runs:
ACP_PROMPT_TIMEOUT: ${{ inputs.acp-prompt-timeout }}
LLM_MODEL: ${{ steps.select-model.outputs.selected_model }}
LLM_BASE_URL: ${{ inputs.llm-base-url }}
+ LLM_EXTRA_BODY: ${{ inputs.llm-extra-body }}
+ MAX_ITERATIONS: ${{ inputs.max-iterations }}
REVIEW_STYLE: ${{ inputs.review-style }}
REQUIRE_EVIDENCE: ${{ inputs.require-evidence }}
COLLECT_FEEDBACK: ${{ inputs.collect-feedback }}
diff --git a/plugins/pr-review/scripts/agent_script.py b/plugins/pr-review/scripts/agent_script.py
index c8104773..2abdfb70 100644
--- a/plugins/pr-review/scripts/agent_script.py
+++ b/plugins/pr-review/scripts/agent_script.py
@@ -27,6 +27,8 @@
LLM_API_KEY: API key for the LLM (required for OpenHands agent kind)
LLM_MODEL: Language model to use (default: anthropic/claude-sonnet-4-5-20250929)
LLM_BASE_URL: Optional base URL for LLM API
+ LLM_EXTRA_BODY: Optional JSON object of extra request-body fields for the LLM
+ MAX_ITERATIONS: Optional cap on agent iterations per review run
GITHUB_TOKEN: GitHub token for API access (required)
PR_NUMBER: Pull request number (required)
PR_TITLE: Pull request title (required)
@@ -893,6 +895,30 @@ def validate_environment() -> dict[str, Any]:
sys.exit(1)
api_key = os.getenv("LLM_API_KEY")
+
+ extra_body: dict[str, Any] = {}
+ raw_extra_body = os.getenv("LLM_EXTRA_BODY", "")
+ if raw_extra_body:
+ try:
+ extra_body = json.loads(raw_extra_body)
+ except json.JSONDecodeError as exc:
+ logger.error(f"LLM_EXTRA_BODY is not valid JSON: {exc}")
+ sys.exit(1)
+ if not isinstance(extra_body, dict):
+ logger.error("LLM_EXTRA_BODY must be a JSON object")
+ sys.exit(1)
+
+ max_iterations: int | None = None
+ raw_max_iterations = os.getenv("MAX_ITERATIONS", "")
+ if raw_max_iterations:
+ try:
+ max_iterations = int(raw_max_iterations)
+ except ValueError:
+ logger.error("MAX_ITERATIONS must be an integer")
+ sys.exit(1)
+ if max_iterations < 1:
+ logger.error("MAX_ITERATIONS must be at least 1")
+ sys.exit(1)
if agent_kind == "openhands" and not api_key:
logger.error(
"LLM_API_KEY is required when AGENT_KIND is 'openhands'"
@@ -925,6 +951,8 @@ def validate_environment() -> dict[str, Any]:
"github_token": os.getenv("GITHUB_TOKEN"),
"model": os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"),
"base_url": os.getenv("LLM_BASE_URL"),
+ "extra_body": extra_body,
+ "max_iterations": max_iterations,
"require_evidence": _get_bool_env("REQUIRE_EVIDENCE"),
"collect_feedback": _get_bool_env("COLLECT_FEEDBACK"),
"review_run_url": os.getenv("REVIEW_RUN_URL", ""),
@@ -1076,6 +1104,8 @@ def create_conversation(
}
if config["base_url"]:
llm_config["base_url"] = config["base_url"]
+ if config["extra_body"]:
+ llm_config["litellm_extra_body"] = config["extra_body"]
llm = LLM(**llm_config)
@@ -1114,6 +1144,8 @@ def create_conversation(
conversation_kwargs["visualizer"] = DelegationVisualizer(
name="PR Review Coordinator"
)
+ if config["max_iterations"]:
+ conversation_kwargs["max_iteration_per_run"] = config["max_iterations"]
return Conversation(**conversation_kwargs)