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..5b97f1a7 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 @@ -258,6 +274,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)