Skip to content

fix(agent-server): keep the managed-key refresh hook after an LLM switch - #5904

Merged
enyst merged 1 commit into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-managed-key-switch
Oct 22, 2026
Merged

enyst merged 1 commit into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-managed-key-switch

Conversation

@enyst

@enyst enyst commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

HUMAN:
This PR proposes a fix for a bug uncovered during the first full agentic run of verify-agent-server.

(totally human) This is interesting. I reported a couple of bugs with this a while ago, managed LLM lost auth after switch, and I assumed the fix would be in enterprise, but I wasn't aware that managed refresh happens here. Or maybe it happens in the sdk because the bug was fixed only for conversation start, not for switch.


AGENT:

Summary

REST or tool-driven switch_llm / switch_profile
  └─ state.agent = replacement agent
       └─ EventService state-change callback
            ├─ register_managed_llm_key_refresh(new agent)
            │    ├─ enabled + allow-listed base_url → attach refresh hook
            │    └─ disabled / BYOK scope           → no hook
            └─ publish the state update

Registration also covers switches deferred until the step ends.

Why

An LLM switch should retain managed-key refresh instead of failing the next eligible 401.

Issue Number

Fixes #5890

How to Test

Previously recorded verification:

Reproduction commands, results, and limits
  • uv run pytest tests/agent_server/test_managed_llm_key.py - new test fails on main (assert None is not None), passes here; 17 passed. Also test_managed_llm_key_http.py, test_conversation_router.py, test_event_service.py, test_event_streaming.py, test_conversation_service.py: all pass.
  • Map proof: control-agent-server map run --file F12 --only F12.managed-key-switch --fresh --repeat 2 --checkout <main> -> xfail x2 on main (6e7a853); same from this branch -> xpass x2. After dropping the marker: map check clean and map run --only F12.managed-key-switch --fresh -> pass. F12.managed-key-refresh and F12.managed-key-scope still pass.
  • pre-commit on the changed files: clean.

Video/Screenshots

No UI change; reproduction evidence is above.

Type

  • Bug fix

Notes

This stays in Agent Server; SDK switch methods are unchanged. The separate title-LLM bypass remains #5528 (discussion). Tracked under #5653.

Visual explanation by Codex using HumanLayer show-me.


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 python-node-runtime Link
python-minimal amd64, arm64 python-node-runtime Link
python amd64, arm64 python-node-runtime Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:7a59803-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-7a59803-python \
  ghcr.io/openhands/agent-server:7a59803-python

All tags pushed for this build

ghcr.io/openhands/agent-server:7a59803-golang-amd64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-golang-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-golang-amd64
ghcr.io/openhands/agent-server:7a59803-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:7a59803-golang-arm64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-golang-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-golang-arm64
ghcr.io/openhands/agent-server:7a59803-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:7a59803-java-amd64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-java-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-java-amd64
ghcr.io/openhands/agent-server:7a59803-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:7a59803-java-arm64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-java-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-java-arm64
ghcr.io/openhands/agent-server:7a59803-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:7a59803-python-amd64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-amd64
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-amd64
ghcr.io/openhands/agent-server:7a59803-python-arm64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-arm64
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-arm64
ghcr.io/openhands/agent-server:7a59803-python-minimal-amd64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-minimal-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-minimal-amd64
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:7a59803-python-minimal-arm64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-minimal-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-minimal-arm64
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:7a59803-python-slim-amd64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-slim-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-slim-amd64
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:7a59803-python-slim-arm64
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-slim-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-slim-arm64
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:7a59803-golang
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-golang
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-golang
ghcr.io/openhands/agent-server:7a59803-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:7a59803-java
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-java
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-java
ghcr.io/openhands/agent-server:7a59803-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:7a59803-python-minimal
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-minimal
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-minimal
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-minimal
ghcr.io/openhands/agent-server:7a59803-python-slim
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python-slim
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python-slim
ghcr.io/openhands/agent-server:7a59803-python-node-runtime-slim
ghcr.io/openhands/agent-server:7a59803-python
ghcr.io/openhands/agent-server:7a598030f9145ed47cf56c9d32d99f23f4367df9-python
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-managed-key-switch-python
ghcr.io/openhands/agent-server:7a59803-python-node-runtime

About Multi-Architecture Support

  • Each variant tag (e.g., 7a59803-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 7a59803-python-amd64) are also available if needed

switch_llm / switch_profile (REST endpoints and the agent's switch tools)
install a new LLM object, and the refresh-on-401 hook was registered only
when the conversation was loaded. The server's state-change callback now
re-runs register_managed_llm_key_refresh when the conversation's agent
changes, so an allow-listed switched LLM refreshes its key on a 401.
The F12.managed-key-switch map bullet drops its known-bug marker.

Fixes #5890

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MKndVj7XWvRQmvW9w2bDg
@enyst enyst self-assigned this Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   event_service.py103912188%155, 161–162, 281–282, 405, 409, 414, 441, 448, 482, 485–486, 490–491, 502, 508, 537–539, 542–543, 632–633, 754, 774, 801, 825–826, 830, 838, 841, 857, 891, 895–896, 930, 937, 943, 1003, 1006, 1021–1022, 1128, 1158, 1161, 1239, 1257, 1271, 1431–1432, 1475, 1512, 1652–1655, 1659, 1702, 1706, 1713, 1727, 1741, 1765, 1812–1814, 1898, 1923, 1929, 1931, 1941, 1943, 1950, 1958, 1960–1961, 1965, 2013, 2019, 2028–2029, 2035–2038, 2046–2049, 2098–2100, 2160–2161, 2163–2170, 2172–2173, 2182–2183, 2185–2186, 2193–2194, 2196–2197, 2208, 2233, 2239, 2245, 2254–2255, 2277, 2287
TOTAL497461981660% 

@enyst
enyst marked this pull request as ready for review October 14, 2026 02:06
@enyst
enyst requested a review from all-hands-bot October 14, 2026 02:12

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was posted by an AI agent (OpenHands).

Scope

The change belongs in this repository: it fixes agent-server behavior (the managed-key refresh hook for the Agent Server's REST/WebSocket lifecycle), touches only openhands-agent-server/ plus a test and a skill feature-map reference, and does not cross the ownership boundary into OpenHands/OpenHands, OpenHands/extensions, or OpenHands/automation.

What I verified

The root cause matches the linked issue #5890: register_managed_llm_key_refresh ran only from EventService.start, while LocalConversation.switch_llm (and switch_profile, plus the switch_llm / route_task_to_model tools, which both route through switch_llm) installs a fresh LLM that carries no hook.

The fix is minimal and reuses the existing idempotent, fail-closed mechanism rather than adding a parallel hook or allow-list:

  • EventService._state_change_callback wraps the previous conversation._on_event target. On a ConversationStateUpdateEvent with key == "agent" it re-runs register_managed_llm_key_refresh(conversation._state.agent) and then forwards the event exactly as before, so state publishing to WebSocket subscribers is unchanged.
  • Every switch path assigns state.agent, so the callback covers the REST endpoints and the agent tools. I confirmed the assignment in LocalConversation.switch_llm (which switch_profile delegates to) and in the two switch tools.
  • I confirmed empirically on the head that the callback fires for a real EventService + switch_llm, that an in-scope switched LLM ends up with a hook that feeds _resolve_refreshed_api_key, that a BYOK base_url gets no hook, and that with OH_LLM_API_KEY_REFRESH_URL unset nothing is registered.

Checks on the exact head 7a598030 are green (agent-server-tests, pre-commit, REST API/OpenAPI, integration, security, etc.). Focused tests pass locally: tests/agent_server/test_managed_llm_key.py (17), test_managed_llm_key_http.py + test_event_service.py (149), test_conversation_router.py + test_event_streaming.py + test_conversation_service.py (246). The acceptance criteria in #5890 are met, including the non-persistence of the hook and unchanged reload behavior.

Notes (non-blocking)

  • The linked issue's "live evidence" criterion is satisfied by the author's verify-agent-server map run (F12.managed-key-switch xfail x2 on main, xpass x2 here, then pass after dropping the # bug marker); this is a production lifecycle path, and that run exercises the real entry point. CI on this head also passes the agent-server integration/stress checks.
  • The related title-LLM gap (#5528) is explicitly a non-goal here and remains open; no action needed for this PR.

No material correctness, security, compatibility, or acceptance-criterion defect found.

✅ APPROVED

@enyst
enyst merged commit 834ebcf into main Oct 22, 2026
74 of 75 checks passed
@enyst
enyst deleted the claude/umbrella-bug-fixes-tgz9ai-managed-key-switch branch October 22, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: managed LLM key refresh hook is lost after switch_llm / switch_profile, so the switched LLM's 401 is not refreshed

3 participants