OpenHands / OpenHands/extensions
PR reviewer and repo monitor skills ignore AUTOMATION_MODEL — automations run on the active profile, not their configured one
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 148
- Forks
- 90
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 36
Description
Summary
github-pr-reviewer and github-repo-monitor build their conversation payload from the agent server's current settings instead of the automation's configured LLM profile. Neither script reads AUTOMATION_MODEL:
$ grep -c AUTOMATION_MODEL skills/github-pr-reviewer/scripts/main.py skills/github-repo-monitor/scripts/main.py
0
0
The result: an automation created from these skills ignores the profile selected for it and runs on whatever profile happens to be active in the UI at the moment it fires.
Where
skills/github-pr-reviewer/scripts/main.py:301-308
def _get_agent_dict(agent_url: str, api_key: str) -> dict:
data = _fetch_settings(agent_url, api_key)
llm = data.get("agent_settings", {}).get("llm", {})
return {
"kind": "Agent",
"llm": llm,
...
skills/github-repo-monitor/scripts/main.py:439-452 has the same _get_agent_dict().
agent_settings.llm from GET /api/settings is the active profile, not the automation's.
This contradicts the documented contract
skills/openhands-automation/references/custom-automation.md:277-293 states that the service injects AUTOMATION_MODEL with the selected profile name and that scripts must honor it:
Calling
workspace.get_llm()with noprofile_namealways uses the user's default LLM. The built-in prompt and plugin presets already follow the pattern above; custom scripts should too so the selected profile is honored regardless of trigger type or execution backend.
The service side works as documented — openhands/automation/dispatcher.py:253-254 sets env_vars["AUTOMATION_MODEL"], and the built-in presets (presets/prompt/sdk_main.py:95,290) consume it correctly. Only these two skill templates skip the step.
Observed on our shared OSS automation instance
agent-canvas 1.6.1, openhands-automation 1.3.1, local mode.
A triage automation generated from this pattern has profile gpt-5.6-sol configured. Every conversation it created today ran on a different model:
04:13:58Z openai/gpt-5.5 usage_id=default
04:19:58Z openai/gpt-5.5
04:36:00Z openai/gpt-5.5
04:39:00Z openai/gpt-5.5
05:37:04Z openai/gpt-5.5
openai/gpt-5.5 was the active profile in the UI during that window. The inverse case confirms the mechanism: a sibling automation with no profile configured ran on openai/gpt-5.6-sol the previous evening — again the then-active profile. Runs from a preset-based automation on the same host, over the same dispatcher, honored their profile correctly:
AUTOMATION_MODEL: gpt-5.6-sol
=== GET_LLM ===
profile: gpt-5.6-sol
model: openai/gpt-5.6-sol
So the environment variable is delivered to these runs; the scripts just never read it.
Impact
- The per-automation LLM profile selector is silently a no-op for anything built from these two skills.
- Because the LLM is read live at fire time, anyone switching the active profile in the UI silently changes which model every such automation uses — on a shared instance, without touching any automation.
- PR review quality and cost drift with no signal in the run record. This blocks the outcome requested in OpenHands/automation#222.
Suggested fix
Resolve the profile when AUTOMATION_MODEL is set, and fall back to current behavior when it is not:
def _get_agent_dict(agent_url: str, api_key: str) -> dict:
data = _fetch_settings(agent_url, api_key)
llm = data.get("agent_settings", {}).get("llm", {})
profile = os.environ.get("AUTOMATION_MODEL")
if profile:
req = urllib.request.Request(
f"{agent_url}/api/profiles/{quote(profile, safe='')}",
headers={"X-Session-API-Key": api_key, "X-Expose-Secrets": "plaintext"},
)
try:
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as r:
llm = json.loads(r.read())["config"]
llm["usage_id"] = f"profile:{profile}"
except urllib.error.HTTPError as exc:
if exc.code != 404:
raise
# profile renamed or deleted after the automation was created
print(f"profile {profile!r} not found; falling back to active profile")
return {"kind": "Agent", "llm": llm, ...}
Two implementation notes:
X-Expose-Secrets: plaintextis required — without itconfig.api_keycomes backnull. This mirrors what the SDK does inopenhands/sdk/workspace/remote/base.py:364-380.StartConversationRequestexposestitle_llm_profileandagent_profile_idbut no field for the agent's LLM profile, so passing the resolved config inline is the only route over the REST API.
Worth auditing the other skills that create conversations directly for the same pattern.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with _get_agent_dict() in skills/github-pr-reviewer/scripts/main.py:301-308 and skills/github-repo-monitor/scripts/main.py:439-452, then read custom-automation.md:277-293 and the dispatcher and SDK references it names. Trace how AUTOMATION_MODEL reaches each script and how the profile endpoint returns its config. Done means both skills honor the configured profile while preserving fallback behavior when it is absent or unavailable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100