diff --git a/AGENTS.md b/AGENTS.md
index 7d4aef35e4..e07c05408a 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.28.1")
+ - `OH_AGENT_SERVER_VERSION` — specific PyPI version (e.g., "1.27.0")
- `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.28.1` for agent-server SDK libraries
+ - Default: released PyPI version `1.27.0` 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 03c5033767..3ca4b23b1d 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,46 +625,6 @@ 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 ee062d2d2e..9bfc8e8638 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.28.1",
+ "openhands-agent-server==1.27.0",
"--with",
- "openhands-sdk==1.28.1",
+ "openhands-sdk==1.27.0",
"--with",
- "openhands-tools==1.28.1",
+ "openhands-tools==1.27.0",
"--with",
- "openhands-workspace==1.28.1",
+ "openhands-workspace==1.27.0",
"agent-server",
]);
- expect(cmd.source).toBe("PyPI (1.28.1, default)");
+ expect(cmd.source).toBe("PyPI (1.27.0, 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 e6fdfa971b..b30a64ab41 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.28.1",
+ "agentServer": "1.27.0",
"agentCanvas": "1.0.0-rc.6",
- "automation": "1.0.0a9",
- "automationSdk": "1.28.1",
+ "automation": "1.0.0a7",
+ "automationSdk": "1.27.0",
"githubMcpServer": "1.2.0"
},
diff --git a/package-lock.json b/package-lock.json
index df22f98b84..34a3d55737 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.1",
+ "@openhands/extensions": "0.4.0",
"@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.1",
- "resolved": "https://registry.npmjs.org/@openhands/extensions/-/extensions-0.4.1.tgz",
- "integrity": "sha512-wpoLYiuy4aiaiLeJpUdkoc2oK861/u5uc8rvZD9s+98fDS8vjnDSImEIG35XKsKT6xMsKlOK6pTpdgNfb8sB8w==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@openhands/extensions/-/extensions-0.4.0.tgz",
+ "integrity": "sha512-cz0k8/rl/rfsD8rAatU1P0SOj/LKaSy+aUu9qp11yEfl0yF2D1vltY4XYa84LmTkTwxtOwDbKtqRPdZ6GJ5b/A==",
"license": "MIT",
"engines": {
"node": ">=18.20.0"
diff --git a/package.json b/package.json
index 5b7a31e807..4a24c1235d 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.1",
+ "@openhands/extensions": "0.4.0",
"@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 b21e78cd57..582ef0c9aa 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.28.1 node scripts/check-sdk-version-sync.mjs
+ * EXPECTED_SDK_VERSION=1.27.0 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.28.1"}}'
+ -d '{"event_type": "sdk-version-check", "client_payload": {"version": "1.27.0"}}'
`);
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.28.1,<2.0.0"
- * "openhands-tools==1.28.1"
- * "openhands-workspace (>=1.28.1)"
+ * "openhands-sdk>=1.27.0,<2.0.0"
+ * "openhands-tools==1.27.0"
+ * "openhands-workspace (>=1.27.0)"
*/
function parseSdkVersionsFromRequiresDist(requiresDist) {
const versions = {};
@@ -284,7 +284,7 @@ function parseSdkVersionsFromRequiresDist(requiresDist) {
}
// Extract the version number - look for patterns like:
- // ">=1.28.1", "==1.28.1", "(>=1.28.1)", "~=1.28.1"
+ // ">=1.27.0", "==1.27.0", "(>=1.27.0)", "~=1.27.0"
// 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 284d1776e9..cac5b7ebc8 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.28.1")
+ * - OH_AGENT_SERVER_VERSION: Specific PyPI version (e.g., "1.27.0")
*
* 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 2909785410..3309671f4a 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,26 +255,13 @@ 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 local
+ // SDK has already rewritten to `litellm_proxy/*` — because older 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") {
- 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) {
+ if (isOpenHandsProxyModel(llmConfig.model, llmConfig.base_url)) {
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 5a66ed0eff..706387237b 100644
--- a/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts
+++ b/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts
@@ -70,46 +70,6 @@ async function getProfileConfig(
return (data.config ?? {}) as Record;
}
-/**
- * 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.configure({ mode: "serial" });
// ═══════════════════════════════════════════════════════════════════════
@@ -397,13 +357,8 @@ 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
// paired with the All-Hands proxy base URL.
- //
- // Agent-server ≥1.28 additionally normalises litellm_proxy/* back to
- // openhands/* on storage and manages the proxy URL internally (returning
- // base_url:null). The assertions below accept both representations.
const PROXY_PROFILE = "proxy-base-url-test";
const LITELLM_PROXY_MODEL = "litellm_proxy/claude-opus-4-8";
- const OPENHANDS_EQUIVALENT_MODEL = "openhands/claude-opus-4-8";
const OPENHANDS_PROXY_BASE_URL = "https://llm-proxy.app.all-hands.dev/";
test.beforeEach(async ({ page }) => {
@@ -496,15 +451,15 @@ test.describe("litellm_proxy proxy base_url preservation", () => {
await waitForTestId(page, "add-llm-profile");
});
- // ── Verify: the proxy setup survived the Basic-tab save ──
- await test.step("verify proxy setup is preserved after save", async () => {
+ // ── Verify: the proxy base_url survived the Basic-tab save ──
+ await test.step("verify base_url is preserved after save", async () => {
const config = await getProfileConfig(request, PROXY_PROFILE);
- assertProxyProfileConfig(
- config,
- LITELLM_PROXY_MODEL,
- OPENHANDS_EQUIVALENT_MODEL,
- OPENHANDS_PROXY_BASE_URL,
- );
+ 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);
});
// ── Verify: the profile also looks correct after a page reload ──
@@ -514,12 +469,8 @@ test.describe("litellm_proxy proxy base_url preservation", () => {
// Re-read via API to confirm persistence is durable
const config = await getProfileConfig(request, PROXY_PROFILE);
- assertProxyProfileConfig(
- config,
- LITELLM_PROXY_MODEL,
- OPENHANDS_EQUIVALENT_MODEL,
- OPENHANDS_PROXY_BASE_URL,
- );
+ expect(config.base_url).toBe(OPENHANDS_PROXY_BASE_URL);
+ expect(config.model).toBe(LITELLM_PROXY_MODEL);
});
});
});