From 910b19ae76845cd269e1e06b2e59823946904cba Mon Sep 17 00:00:00 2001 From: Tim O'Farrell Date: Thu, 11 Jun 2026 17:16:03 -0600 Subject: [PATCH] =?UTF-8?q?chore:=20bump=20agent-server=20=E2=86=92=201.28?= =?UTF-8?q?.1,=20automation=20=E2=86=92=201.0.0a9,=20extensions=20?= =?UTF-8?q?=E2=86=92=200.4.1=20(#1319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: bump agent-server → 1.28.1, automation → 1.0.0a9, extensions → 0.4.1 Co-authored-by: openhands * Test fixes * fix: inject proxy base_url for litellm_proxy/* when server omits it (agent-server ≥1.28) Agent-server ≥1.28 may return base_url:null when fetching a litellm_proxy/* profile config, even when the profile was saved with the All-Hands proxy URL. This caused the Basic-tab re-save flow in LlmSettingsLocalView.handleSave to call isOpenHandsProxyModel(model, null) → false, hitting the else-branch that deletes base_url and stranding the profile (issue #1146). Fix: add a secondary check — litellm_proxy/* with a missing base_url is treated the same as litellm_proxy/* with the proxy URL already set, and OPENHANDS_LLM_PROXY_BASE_URL is injected before the save request is sent. Also updates the mock-LLM E2E test to accept both storage representations: - litellm_proxy/* + proxyBaseUrl (pre-1.28, guards issue #1146 regression) - openhands/* + null (1.28+, server-managed routing) And adds a unit test exercising the base_url:null path. Co-authored-by: openhands --------- Co-authored-by: openhands --- AGENTS.md | 4 +- .../llm-settings-local-view.test.tsx | 40 +++++++++++ __tests__/scripts/dev-safe.test.ts | 10 +-- config/defaults.json | 6 +- package-lock.json | 8 +-- package.json | 2 +- scripts/check-sdk-version-sync.mjs | 12 ++-- scripts/dev-safe.mjs | 2 +- .../llm-profiles/llm-settings-local-view.tsx | 17 ++++- .../mock-llm-profile-management.spec.ts | 69 ++++++++++++++++--- 10 files changed, 136 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e07c05408a..7d4aef35e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -521,10 +521,10 @@ When adding code that needs a new string, decide up front which rule it falls un - `scripts/dev-safe.mjs` uses `uvx` for temporary agent-server installation — no permanent `uv tool install` needed. Environment variables (highest precedence first): - `OH_AGENT_SERVER_LOCAL_PATH` — absolute path to a local `software-agent-sdk` checkout. Runs the local checkout via `uvx` with `--with-editable` for `openhands-sdk`/`openhands-tools`/`openhands-workspace` and `--reinstall` for `openhands-agent-server`, so SDK edits are picked up on restart. Highest precedence. - `OH_AGENT_SERVER_GIT_REF` — git commit SHA or branch name (takes precedence over version) - - `OH_AGENT_SERVER_VERSION` — specific PyPI version (e.g., "1.27.0") + - `OH_AGENT_SERVER_VERSION` — specific PyPI version (e.g., "1.28.1") - `OH_SECRET_KEY` — secret key for settings encryption; auto-generated and persisted to `~/.openhands/agent-canvas/secret-key.txt` on first run (same file Docker uses), ensuring dev mode and Docker share the same key when both mount the same `~/.openhands` directory. Override with the env var to pin a specific key. - `SESSION_API_KEY` / `OH_SESSION_API_KEYS_0` / `VITE_SESSION_API_KEY` — session API key for agent-server authentication; auto-generated using `crypto.randomBytes(32)` if not set, passed to both agent-server (`OH_SESSION_API_KEYS_0`) and frontend (`VITE_SESSION_API_KEY`) - - Default: released PyPI version `1.27.0` for agent-server SDK libraries + - Default: released PyPI version `1.28.1` for agent-server SDK libraries - Security: `scripts/dev-safe.mjs` and `scripts/dev-with-automation.mjs` auto-generate random API keys when needed and persist the defaults so static builds, localStorage, and restarted services stay in sync: - `SESSION_API_KEY` — 64-character hex (256-bit) for agent-server API authentication; persisted at `~/.openhands/agent-canvas/session-api-key.txt` unless overridden via env var diff --git a/__tests__/components/settings/llm-profiles/llm-settings-local-view.test.tsx b/__tests__/components/settings/llm-profiles/llm-settings-local-view.test.tsx index 3ca4b23b1d..03c5033767 100644 --- a/__tests__/components/settings/llm-profiles/llm-settings-local-view.test.tsx +++ b/__tests__/components/settings/llm-profiles/llm-settings-local-view.test.tsx @@ -625,6 +625,46 @@ describe("LlmSettingsLocalView", () => { expect(savedLlm.model).toBe("litellm_proxy/claude-opus-4-8"); expect(savedLlm.base_url).toBe(OPENHANDS_LLM_PROXY_BASE_URL); }); + + it("injects the proxy base_url for a litellm_proxy model when server omits it (agent-server ≥1.28)", async () => { + // Arrange — agent-server ≥1.28 may omit the default base_url from stored + // profile configs. When the profile is fetched with base_url:null the save + // must still inject the All-Hands proxy URL, otherwise the profile is + // stranded on the next save from the Basic tab. + const user = userEvent.setup(); + vi.mocked(ProfilesService.getProfile).mockResolvedValue({ + name: "gpt-4-profile", + api_key_set: true, + config: { + model: "litellm_proxy/claude-opus-4-8", + api_key: "gAAAA_encrypted_key", + base_url: null, + }, + }); + mockSaveMutateAsync.mockResolvedValueOnce({ success: true }); + + renderWithProviders(); + + await user.click(screen.getAllByTestId("profile-menu-trigger")[0]); + await user.click(screen.getByTestId("profile-edit")); + await waitFor(() => { + expect(screen.getByTestId("profile-name-input")).toHaveValue( + "gpt-4-profile", + ); + }); + await user.click(await screen.findByTestId("sdk-section-basic-toggle")); + await waitFor(() => { + expect(screen.getByTestId("save-profile-btn")).not.toBeDisabled(); + }); + await user.click(screen.getByTestId("save-profile-btn")); + + // Assert — even though the server returned base_url:null, the Basic-tab + // save must inject the proxy URL so the profile stays usable. + await waitFor(() => expect(mockSaveMutateAsync).toHaveBeenCalled()); + const savedLlm = mockSaveMutateAsync.mock.calls[0][0].request.llm; + expect(savedLlm.model).toBe("litellm_proxy/claude-opus-4-8"); + expect(savedLlm.base_url).toBe(OPENHANDS_LLM_PROXY_BASE_URL); + }); }); describe("All tab save", () => { diff --git a/__tests__/scripts/dev-safe.test.ts b/__tests__/scripts/dev-safe.test.ts index 9bfc8e8638..ee062d2d2e 100644 --- a/__tests__/scripts/dev-safe.test.ts +++ b/__tests__/scripts/dev-safe.test.ts @@ -389,16 +389,16 @@ describe("buildAgentServerCommand", () => { // Defaults to the released PyPI version with all SDK packages pinned to same version expect(cmd.args).toEqual([ "--from", - "openhands-agent-server==1.27.0", + "openhands-agent-server==1.28.1", "--with", - "openhands-sdk==1.27.0", + "openhands-sdk==1.28.1", "--with", - "openhands-tools==1.27.0", + "openhands-tools==1.28.1", "--with", - "openhands-workspace==1.27.0", + "openhands-workspace==1.28.1", "agent-server", ]); - expect(cmd.source).toBe("PyPI (1.27.0, default)"); + expect(cmd.source).toBe("PyPI (1.28.1, default)"); }); it("uses specific PyPI version when OH_AGENT_SERVER_VERSION is set with all packages pinned", () => { diff --git a/config/defaults.json b/config/defaults.json index b30a64ab41..e6fdfa971b 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -2,10 +2,10 @@ "_comment": "Single source of truth for version pins, ports, paths, and defaults shared across the npm and Docker install paths. Read by scripts/dev-safe.mjs, scripts/dev-with-automation.mjs, docker/entrypoint.sh (via generated defaults.env), and .github/workflows/docker.yml.", "versions": { - "agentServer": "1.27.0", + "agentServer": "1.28.1", "agentCanvas": "1.0.0-rc.6", - "automation": "1.0.0a7", - "automationSdk": "1.27.0", + "automation": "1.0.0a9", + "automationSdk": "1.28.1", "githubMcpServer": "1.2.0" }, diff --git a/package-lock.json b/package-lock.json index 34a3d55737..df22f98b84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@heroui/react": "2.8.10", "@microlink/react-json-view": "1.31.20", "@monaco-editor/react": "4.7.0", - "@openhands/extensions": "0.4.0", + "@openhands/extensions": "0.4.1", "@openhands/typescript-client": "1.24.3", "@react-router/node": "7.17.0", "@react-router/serve": "7.17.0", @@ -3468,9 +3468,9 @@ "license": "MIT" }, "node_modules/@openhands/extensions": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@openhands/extensions/-/extensions-0.4.0.tgz", - "integrity": "sha512-cz0k8/rl/rfsD8rAatU1P0SOj/LKaSy+aUu9qp11yEfl0yF2D1vltY4XYa84LmTkTwxtOwDbKtqRPdZ6GJ5b/A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@openhands/extensions/-/extensions-0.4.1.tgz", + "integrity": "sha512-wpoLYiuy4aiaiLeJpUdkoc2oK861/u5uc8rvZD9s+98fDS8vjnDSImEIG35XKsKT6xMsKlOK6pTpdgNfb8sB8w==", "license": "MIT", "engines": { "node": ">=18.20.0" diff --git a/package.json b/package.json index 4a24c1235d..5b7a31e807 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@heroui/react": "2.8.10", "@microlink/react-json-view": "1.31.20", "@monaco-editor/react": "4.7.0", - "@openhands/extensions": "0.4.0", + "@openhands/extensions": "0.4.1", "@openhands/typescript-client": "1.24.3", "@react-router/node": "7.17.0", "@react-router/serve": "7.17.0", diff --git a/scripts/check-sdk-version-sync.mjs b/scripts/check-sdk-version-sync.mjs index 582ef0c9aa..b21e78cd57 100644 --- a/scripts/check-sdk-version-sync.mjs +++ b/scripts/check-sdk-version-sync.mjs @@ -19,7 +19,7 @@ * * Usage: * node scripts/check-sdk-version-sync.mjs - * EXPECTED_SDK_VERSION=1.27.0 node scripts/check-sdk-version-sync.mjs + * EXPECTED_SDK_VERSION=1.28.1 node scripts/check-sdk-version-sync.mjs * node scripts/check-sdk-version-sync.mjs --check-pypi * * Environment variables: @@ -79,7 +79,7 @@ Triggering from other repos: -H "Authorization: token \$GITHUB_TOKEN" \\ -H "Accept: application/vnd.github.v3+json" \\ https://api.github.com/repos/OpenHands/agent-canvas/dispatches \\ - -d '{"event_type": "sdk-version-check", "client_payload": {"version": "1.27.0"}}' + -d '{"event_type": "sdk-version-check", "client_payload": {"version": "1.28.1"}}' `); process.exit(0); } @@ -268,9 +268,9 @@ async function fetchPyPIDependencies(packageName, version) { * Parse PyPI requires_dist array and extract SDK package versions * * PyPI returns dependencies in PEP 508 format like: - * "openhands-sdk>=1.27.0,<2.0.0" - * "openhands-tools==1.27.0" - * "openhands-workspace (>=1.27.0)" + * "openhands-sdk>=1.28.1,<2.0.0" + * "openhands-tools==1.28.1" + * "openhands-workspace (>=1.28.1)" */ function parseSdkVersionsFromRequiresDist(requiresDist) { const versions = {}; @@ -284,7 +284,7 @@ function parseSdkVersionsFromRequiresDist(requiresDist) { } // Extract the version number - look for patterns like: - // ">=1.27.0", "==1.27.0", "(>=1.27.0)", "~=1.27.0" + // ">=1.28.1", "==1.28.1", "(>=1.28.1)", "~=1.28.1" // After the package name and before any comma or closing paren const versionPattern = /[><=~!]+\s*([0-9]+(?:\.[0-9]+)*)/; const match = dep.match(versionPattern); diff --git a/scripts/dev-safe.mjs b/scripts/dev-safe.mjs index cac5b7ebc8..284d1776e9 100644 --- a/scripts/dev-safe.mjs +++ b/scripts/dev-safe.mjs @@ -394,7 +394,7 @@ export function validateFrontendDependencies( * edits are picked up without a manual reinstall. The agent-server itself * is rebuilt from local source on each invocation (--reinstall). * - OH_AGENT_SERVER_GIT_REF: Git commit SHA or branch name - * - OH_AGENT_SERVER_VERSION: Specific PyPI version (e.g., "1.27.0") + * - OH_AGENT_SERVER_VERSION: Specific PyPI version (e.g., "1.28.1") * * If none are set, defaults to the released version specified by * DEFAULT_AGENT_SERVER_VERSION. Set OH_AGENT_SERVER_GIT_REF to use a diff --git a/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx b/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx index 3309671f4a..2909785410 100644 --- a/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx +++ b/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx @@ -255,13 +255,26 @@ export function LlmSettingsLocalView() { // The Basic tab has no base_url field; the provider implies it. Persist // the All-Hands proxy explicitly for OpenHands models — including ones the - // SDK has already rewritten to `litellm_proxy/*` — because older local + // SDK has already rewritten to `litellm_proxy/*` — because local // agent-server builds do not infer the LiteLLM proxy api_base on their own, // and dropping it strands the profile as `litellm_proxy/* + base_url:null` // (issue #1146). For other providers, drop any stale custom value and let // the backend use its normal provider defaults. + // + // Agent-server ≥1.28 may omit the default base_url from profile configs, + // causing the stored value to be null even for genuine OpenHands proxy + // models. Treat litellm_proxy/* with a missing base_url the same as + // litellm_proxy/* with the proxy URL already set. if (saveControl.view === "basic") { - if (isOpenHandsProxyModel(llmConfig.model, llmConfig.base_url)) { + const model = llmConfig.model; + const baseUrl = llmConfig.base_url; + const isProxy = isOpenHandsProxyModel(model, baseUrl); + const isLitellmProxyWithMissingBaseUrl = + !isProxy && + typeof model === "string" && + model.startsWith("litellm_proxy/") && + !baseUrl; + if (isProxy || isLitellmProxyWithMissingBaseUrl) { llmConfig.base_url = OPENHANDS_LLM_PROXY_BASE_URL; } else { delete llmConfig.base_url; diff --git a/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts b/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts index 706387237b..ef0e7097d9 100644 --- a/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts +++ b/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts @@ -353,6 +353,46 @@ test.describe("same-model profile identity", () => { // (PR #1148, issue #1146) // ═══════════════════════════════════════════════════════════════════════ +/** + * Assert that a proxy profile config is valid after a Basic-tab re-save. + * + * Agent-server ≥1.28 normalises `litellm_proxy/*` → `openhands/*` on storage + * and manages the proxy URL internally (returning base_url: null). Both + * representations are accepted here. The original issue #1146 regression + * (litellm_proxy/* with null base_url) is still guarded explicitly. + */ +function assertProxyProfileConfig( + config: Record, + litellmProxyModel: string, + openHandsEquivalentModel: string, + proxyBaseUrl: string, +) { + const model = config.model as string | null | undefined; + const baseUrl = config.base_url as string | null | undefined; + + const isLitellmForm = + typeof model === "string" && model.startsWith("litellm_proxy/"); + const isOpenHandsForm = + typeof model === "string" && model.startsWith("openhands/"); + + expect( + isLitellmForm || isOpenHandsForm, + `model must be ${litellmProxyModel} or ${openHandsEquivalentModel}; got: ${model}`, + ).toBe(true); + + if (isLitellmForm) { + // A litellm_proxy/* profile without the proxy URL is stranded (issue #1146). + // The frontend must always inject the URL when saving from the Basic tab. + expect( + baseUrl, + "base_url must be the All-Hands proxy URL for litellm_proxy/* profiles " + + "(dropping it strands the profile — issue #1146)", + ).toBe(proxyBaseUrl); + } + // For openhands/* (agent-server ≥1.28 rewrite), null base_url is correct; + // the server routes the request through the proxy internally. +} + test.describe("litellm_proxy proxy base_url preservation", () => { // Simulates the state the SDK persists after onboarding through the // OpenHands provider: openhands/* is rewritten to litellm_proxy/* and @@ -385,6 +425,11 @@ test.describe("litellm_proxy proxy base_url preservation", () => { page, request, }) => { + // Agent-server ≥1.28 normalises litellm_proxy/* → openhands/* on storage + // and manages the proxy URL internally (returning base_url: null). The old + // assertions hard-coded the pre-1.28 storage format. + const OPENHANDS_EQUIVALENT_MODEL = "openhands/claude-opus-4-8"; + // ── Setup: create a profile with the SDK-rewritten litellm_proxy // model + proxy base_url through the Settings UI, exactly as // the agent-server persists it after an openhands/* model @@ -451,15 +496,15 @@ test.describe("litellm_proxy proxy base_url preservation", () => { await waitForTestId(page, "add-llm-profile"); }); - // ── Verify: the proxy base_url survived the Basic-tab save ── - await test.step("verify base_url is preserved after save", async () => { + // ── Verify: the proxy setup survived the Basic-tab save ── + await test.step("verify proxy setup is preserved after save", async () => { const config = await getProfileConfig(request, PROXY_PROFILE); - expect( - config.base_url, - "base_url must be the All-Hands proxy URL after a Basic-tab re-save; " + - "dropping it strands the profile (issue #1146)", - ).toBe(OPENHANDS_PROXY_BASE_URL); - expect(config.model).toBe(LITELLM_PROXY_MODEL); + assertProxyProfileConfig( + config, + LITELLM_PROXY_MODEL, + OPENHANDS_EQUIVALENT_MODEL, + OPENHANDS_PROXY_BASE_URL, + ); }); // ── Verify: the profile also looks correct after a page reload ── @@ -469,8 +514,12 @@ test.describe("litellm_proxy proxy base_url preservation", () => { // Re-read via API to confirm persistence is durable const config = await getProfileConfig(request, PROXY_PROFILE); - expect(config.base_url).toBe(OPENHANDS_PROXY_BASE_URL); - expect(config.model).toBe(LITELLM_PROXY_MODEL); + assertProxyProfileConfig( + config, + LITELLM_PROXY_MODEL, + OPENHANDS_EQUIVALENT_MODEL, + OPENHANDS_PROXY_BASE_URL, + ); }); }); });