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..9e11fd2632 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
@@ -11,7 +11,6 @@ import * as useLlmProfilesHook from "#/hooks/query/use-llm-profiles";
import * as useActivateLlmProfileHook from "#/hooks/mutation/use-activate-llm-profile";
import * as useSaveLlmProfileHook from "#/hooks/mutation/use-save-llm-profile";
import ProfilesService from "#/api/profiles-service/profiles-service.api";
-import { OPENHANDS_LLM_PROXY_BASE_URL } from "#/utils/openhands-llm";
vi.mock("#/hooks/query/use-llm-profiles");
vi.mock("#/hooks/mutation/use-activate-llm-profile");
@@ -132,7 +131,9 @@ describe("LlmSettingsLocalView", () => {
expect(
screen.getByText(/Add LLM Profile|SETTINGS\$ADD_LLM_PROFILE/),
).toBeInTheDocument();
- expect(screen.getByTestId("profile-editor-description")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("profile-editor-description"),
+ ).toBeInTheDocument();
});
it("returns to list view when back button clicked", async () => {
@@ -193,7 +194,9 @@ describe("LlmSettingsLocalView", () => {
renderWithProviders();
// Error message component should be rendered (text is a translation key)
- expect(screen.getByText("SETTINGS$PROFILES_LOAD_ERROR")).toBeInTheDocument();
+ expect(
+ screen.getByText("SETTINGS$PROFILES_LOAD_ERROR"),
+ ).toBeInTheDocument();
});
/**
@@ -344,9 +347,9 @@ describe("LlmSettingsLocalView", () => {
expect(
screen.getByText(/Edit LLM Profile|SETTINGS\$EDIT_LLM_PROFILE/),
).toBeInTheDocument();
- expect(screen.getByTestId("profile-editor-description")).toHaveTextContent(
- /gpt-4-profile|SETTINGS\$PROFILE_LOADED/,
- );
+ expect(
+ screen.getByTestId("profile-editor-description"),
+ ).toHaveTextContent(/gpt-4-profile|SETTINGS\$PROFILE_LOADED/);
// Verify getProfile was called with the correct profile name
expect(ProfilesService.getProfile).toHaveBeenCalledWith(
@@ -542,11 +545,7 @@ describe("LlmSettingsLocalView", () => {
});
describe("Basic tab save", () => {
- it("persists the OpenHands proxy base_url for OpenHands models", async () => {
- // Arrange — a profile whose stored config pairs an OpenHands model with a
- // stale, non-proxy base_url. Persisting that stale URL is wrong, but
- // older local agent-server builds do not derive the All-Hands proxy when
- // base_url is omitted, so the Basic tab must save the proxy explicitly.
+ it("drops hidden base_url values for OpenHands models", async () => {
const user = userEvent.setup();
vi.mocked(ProfilesService.getProfile).mockResolvedValue({
name: "gpt-4-profile",
@@ -561,7 +560,6 @@ describe("LlmSettingsLocalView", () => {
renderWithProviders();
- // Act — open the profile in edit mode, force the Basic tab, and save.
await user.click(screen.getAllByTestId("profile-menu-trigger")[0]);
await user.click(screen.getByTestId("profile-edit"));
await waitFor(() => {
@@ -575,95 +573,10 @@ describe("LlmSettingsLocalView", () => {
});
await user.click(screen.getByTestId("save-profile-btn"));
- // Assert — the saved LLM config keeps the OpenHands model and replaces
- // the stale base_url with the proxy required for litellm_proxy models.
await waitFor(() => expect(mockSaveMutateAsync).toHaveBeenCalled());
const savedLlm = mockSaveMutateAsync.mock.calls[0][0].request.llm;
expect(savedLlm.model).toBe("openhands/claude-opus-4-5-20251101");
- expect(savedLlm.base_url).toBe(OPENHANDS_LLM_PROXY_BASE_URL);
- });
-
- it("keeps the proxy base_url for a stored litellm_proxy OpenHands model (issue #1146)", async () => {
- // Arrange — the shape actually persisted after onboarding through the
- // OpenHands provider: the SDK has already rewritten openhands/* to
- // litellm_proxy/* and paired it with the All-Hands proxy base URL. A
- // Basic-tab re-save must not strip that base URL, otherwise the profile
- // is stranded as `litellm_proxy/* + base_url:null` and LiteLLM reroutes
- // the OpenHands key to the default OpenAI endpoint.
- 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: OPENHANDS_LLM_PROXY_BASE_URL,
- },
- });
- mockSaveMutateAsync.mockResolvedValueOnce({ success: true });
-
- renderWithProviders();
-
- // Act — open the profile in edit mode, force the Basic tab, and save
- // without touching the model dropdown.
- 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 — the rewritten model is preserved and so is the proxy base URL.
- 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);
- });
-
- 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);
+ expect(savedLlm).not.toHaveProperty("base_url");
});
});
diff --git a/__tests__/utils/openhands-llm.test.ts b/__tests__/utils/openhands-llm.test.ts
index cd03abfea0..4f387d0029 100644
--- a/__tests__/utils/openhands-llm.test.ts
+++ b/__tests__/utils/openhands-llm.test.ts
@@ -1,19 +1,10 @@
import { describe, expect, it } from "vitest";
import {
OPENHANDS_LLM_PROXY_BASE_URL,
- isOpenHandsProviderModel,
isOpenHandsProxyBaseUrl,
- isOpenHandsProxyModel,
} from "#/utils/openhands-llm";
describe("openhands LLM helpers", () => {
- it("identifies OpenHands provider model ids", () => {
- expect(isOpenHandsProviderModel("openhands/gpt-5.5")).toBe(true);
- expect(isOpenHandsProviderModel("litellm_proxy/gpt-5.5")).toBe(false);
- expect(isOpenHandsProviderModel("openai/gpt-4o")).toBe(false);
- expect(isOpenHandsProviderModel(null)).toBe(false);
- });
-
it("exports the All-Hands LiteLLM proxy base URL", () => {
expect(OPENHANDS_LLM_PROXY_BASE_URL).toBe(
"https://llm-proxy.app.all-hands.dev/",
@@ -34,47 +25,4 @@ describe("openhands LLM helpers", () => {
expect(isOpenHandsProxyBaseUrl(null)).toBe(false);
expect(isOpenHandsProxyBaseUrl(undefined)).toBe(false);
});
-
- it("treats both openhands/* and the SDK-rewritten litellm_proxy/* form as OpenHands-backed (issue #1146)", () => {
- // The `openhands/*` form the GUI submits — proxy is implied, base URL is
- // irrelevant.
- expect(isOpenHandsProxyModel("openhands/gpt-5.5", null)).toBe(true);
- expect(isOpenHandsProxyModel("openhands/gpt-5.5", "anything")).toBe(true);
-
- // The `litellm_proxy/*` form the SDK persists, paired with the proxy URL.
- expect(
- isOpenHandsProxyModel(
- "litellm_proxy/claude-opus-4-8",
- OPENHANDS_LLM_PROXY_BASE_URL,
- ),
- ).toBe(true);
- expect(
- isOpenHandsProxyModel(
- "litellm_proxy/claude-opus-4-8",
- "https://llm-proxy.app.all-hands.dev",
- ),
- ).toBe(true);
- });
-
- it("does not over-classify non-OpenHands models as proxy-backed", () => {
- // A litellm_proxy model pointed at a third-party gateway is not OpenHands.
- expect(
- isOpenHandsProxyModel(
- "litellm_proxy/claude-opus-4-8",
- "https://other-proxy.example.com",
- ),
- ).toBe(false);
- // An already-stranded profile (proxy URL lost) is not auto-recognized: it
- // must be re-activated/re-selected, not silently re-stamped here.
- expect(isOpenHandsProxyModel("litellm_proxy/claude-opus-4-8", null)).toBe(
- false,
- );
- // Plain providers stay plain even when oddly paired with the proxy URL.
- expect(
- isOpenHandsProxyModel("openai/gpt-4o", OPENHANDS_LLM_PROXY_BASE_URL),
- ).toBe(false);
- expect(isOpenHandsProxyModel(null, OPENHANDS_LLM_PROXY_BASE_URL)).toBe(
- false,
- );
- });
});
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..fb8be11937 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
@@ -35,10 +35,6 @@ import {
import { BackNavButton } from "#/components/shared/buttons/back-nav-button";
import { Typography } from "#/ui/typography";
import { useSettingsSectionHeader } from "#/contexts/settings-section-header-context";
-import {
- OPENHANDS_LLM_PROXY_BASE_URL,
- isOpenHandsProxyModel,
-} from "#/utils/openhands-llm";
type ViewMode = "list" | "create" | "edit";
@@ -253,32 +249,10 @@ export function LlmSettingsLocalView() {
: {};
const llmConfig: Record = { ...baseConfig, ...dirtyLlm };
- // 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
- // 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.
+ // The Basic tab has no base_url field. Provider defaults are handled by
+ // the backend, so drop stale custom values for every provider.
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) {
- llmConfig.base_url = OPENHANDS_LLM_PROXY_BASE_URL;
- } else {
- delete llmConfig.base_url;
- }
+ delete llmConfig.base_url;
}
// API key handling: an empty value means "no change" (the UX doesn't
diff --git a/src/routes/llm-settings.tsx b/src/routes/llm-settings.tsx
index 5bf21bd0e7..60d21b3b03 100644
--- a/src/routes/llm-settings.tsx
+++ b/src/routes/llm-settings.tsx
@@ -22,10 +22,7 @@ import {
type SettingsView,
} from "#/utils/sdk-settings-schema";
import { DEFAULT_SETTINGS } from "#/services/settings";
-import {
- OPENHANDS_LLM_PROXY_BASE_URL,
- isOpenHandsProxyModel,
-} from "#/utils/openhands-llm";
+import { OPENHANDS_LLM_PROXY_BASE_URL } from "#/utils/openhands-llm";
const LLM_EXCLUDED_KEYS = new Set(["llm.model", "llm.api_key", "llm.base_url"]);
@@ -289,11 +286,7 @@ export function LlmSettingsScreen({
const llm = (agentSettings.llm ?? {}) as Record;
if (context.view === "basic") {
- const model = llm.model ?? context.values["llm.model"];
- const baseUrl = llm.base_url ?? context.values["llm.base_url"];
- llm.base_url = isOpenHandsProxyModel(model, baseUrl)
- ? OPENHANDS_LLM_PROXY_BASE_URL
- : getSchemaFieldDefaultValue(schema, "llm.base_url");
+ llm.base_url = getSchemaFieldDefaultValue(schema, "llm.base_url");
agentSettings.llm = llm;
}
diff --git a/src/utils/openhands-llm.ts b/src/utils/openhands-llm.ts
index 1af7d80121..6431841c7b 100644
--- a/src/utils/openhands-llm.ts
+++ b/src/utils/openhands-llm.ts
@@ -10,12 +10,6 @@ const OPENHANDS_LLM_PROXY_BASE_URLS = new Set([
"https://llm-proxy.app.all-hands.dev/v1",
]);
-const LITELLM_PROXY_PREFIX = "litellm_proxy/";
-
-export function isOpenHandsProviderModel(model: unknown): model is string {
- return typeof model === "string" && model.startsWith("openhands/");
-}
-
/**
* True when `baseUrl` points at the All-Hands LiteLLM proxy, ignoring any
* trailing slash.
@@ -26,22 +20,3 @@ export function isOpenHandsProxyBaseUrl(baseUrl: unknown): baseUrl is string {
OPENHANDS_LLM_PROXY_BASE_URLS.has(baseUrl.trim().replace(/\/+$/, ""))
);
}
-
-/**
- * True for any OpenHands-backed model: the `openhands/*` id the GUI submits, or
- * the `litellm_proxy/*` id the SDK rewrites it to once it is paired with the
- * OpenHands proxy base URL. Both forms must keep the proxy `base_url` on save —
- * dropping it strips the api_base and silently reroutes the request to the
- * default OpenAI endpoint, leaving an unusable profile (issue #1146).
- */
-export function isOpenHandsProxyModel(
- model: unknown,
- baseUrl: unknown,
-): model is string {
- if (isOpenHandsProviderModel(model)) return true;
- return (
- typeof model === "string" &&
- model.startsWith(LITELLM_PROXY_PREFIX) &&
- isOpenHandsProxyBaseUrl(baseUrl)
- );
-}
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 ef0e7097d9..e18e7fe20a 100644
--- a/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts
+++ b/tests/e2e/mock-llm/mock-llm-profile-management.spec.ts
@@ -1,7 +1,7 @@
/**
* Mock-LLM E2E tests: LLM profile management regressions.
*
- * Covers three scenarios that previously had no end-to-end guard:
+ * Covers two scenarios that previously had no end-to-end guard:
*
* 1. Active profile deletion + reconciliation:
* The active LLM profile IS deletable (the PR #1127 disable-guard was
@@ -16,19 +16,10 @@
* user selected, not the first alphabetical match. The fix
* stamps the active profile name on client-side conversation
* metadata at creation and on per-conversation switches.
- *
- * 3. Proxy base_url preservation for litellm_proxy profiles (PR #1148):
- * When the SDK rewrites an `openhands/*` model to `litellm_proxy/*`
- * with the All-Hands proxy base_url, re-saving the profile from the
- * Basic tab must not strip the base_url. Without it the profile is
- * stranded and LiteLLM reroutes requests to the wrong endpoint.
*/
import { test, expect } from "@playwright/test";
import {
- BACKEND_URL,
- SESSION_API_KEY,
- MOCK_LLM_AGENT_URL,
seedLocalStorage,
routeSessionApiKey,
dismissAnalyticsModal,
@@ -48,28 +39,6 @@ import {
const MOCK_MODEL = "openai/mock-test-model";
-/**
- * Read-only helper: fetch a profile's persisted config via the API.
- * Used to verify that UI-driven saves persisted the expected values.
- */
-async function getProfileConfig(
- request: import("@playwright/test").APIRequestContext,
- name: string,
-): Promise> {
- const resp = await request.get(
- `${BACKEND_URL}/api/profiles/${encodeURIComponent(name)}`,
- {
- headers: {
- "X-Session-API-Key": SESSION_API_KEY,
- "X-Expose-Secrets": "encrypted",
- },
- },
- );
- expect(resp.ok(), `GET /api/profiles/${name}: ${resp.status()}`).toBe(true);
- const data = await resp.json();
- return (data.config ?? {}) as Record;
-}
-
test.describe.configure({ mode: "serial" });
// ═══════════════════════════════════════════════════════════════════════
@@ -116,8 +85,14 @@ test.describe("active profile deletion + reconciliation", () => {
await deleteProfileIfExists(page, INACTIVE_PROFILE);
// Create both profiles through the Settings UI
- await createProfileViaUI(page, { profileName: ACTIVE_PROFILE, model: MOCK_MODEL });
- await createProfileViaUI(page, { profileName: INACTIVE_PROFILE, model: MOCK_MODEL });
+ await createProfileViaUI(page, {
+ profileName: ACTIVE_PROFILE,
+ model: MOCK_MODEL,
+ });
+ await createProfileViaUI(page, {
+ profileName: INACTIVE_PROFILE,
+ model: MOCK_MODEL,
+ });
// Activate the first profile through the UI
await activateProfileViaUI(page, ACTIVE_PROFILE);
@@ -289,8 +264,14 @@ test.describe("same-model profile identity", () => {
await deleteProfileIfExists(page, PROFILE_ALPHA);
await deleteProfileIfExists(page, PROFILE_BETA);
- await createProfileViaUI(page, { profileName: PROFILE_ALPHA, model: SHARED_MODEL });
- await createProfileViaUI(page, { profileName: PROFILE_BETA, model: SHARED_MODEL });
+ await createProfileViaUI(page, {
+ profileName: PROFILE_ALPHA,
+ model: SHARED_MODEL,
+ });
+ await createProfileViaUI(page, {
+ profileName: PROFILE_BETA,
+ model: SHARED_MODEL,
+ });
await activateProfileViaUI(page, PROFILE_BETA);
// Register a trajectory for the conversation.
@@ -347,179 +328,3 @@ test.describe("same-model profile identity", () => {
});
});
});
-
-// ═══════════════════════════════════════════════════════════════════════
-// Test 3 — Proxy base_url preservation for litellm_proxy profiles
-// (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
- // paired with the All-Hands proxy base URL.
- const PROXY_PROFILE = "proxy-base-url-test";
- const LITELLM_PROXY_MODEL = "litellm_proxy/claude-opus-4-8";
- const OPENHANDS_PROXY_BASE_URL = "https://llm-proxy.app.all-hands.dev/";
-
- test.beforeEach(async ({ page }) => {
- await seedLocalStorage(page);
- });
-
- test.afterAll(async ({ browser }) => {
- const page = await browser.newPage();
- try {
- await seedLocalStorage(page);
- await routeSessionApiKey(page);
- await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
- await dismissAnalyticsModal(page);
- await waitForTestId(page, "add-llm-profile");
- await deleteProfileIfExists(page, PROXY_PROFILE);
- } catch {
- // best-effort
- } finally {
- await page.close();
- }
- });
-
- test("re-saving a litellm_proxy profile from Basic view preserves the proxy base_url", async ({
- 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
- // selection during onboarding. ──
- await routeSessionApiKey(page);
- await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
- await dismissAnalyticsModal(page);
- await waitForTestId(page, "add-llm-profile");
-
- await deleteProfileIfExists(page, PROXY_PROFILE);
- await createProfileViaUI(page, {
- profileName: PROXY_PROFILE,
- model: LITELLM_PROXY_MODEL,
- baseUrl: OPENHANDS_PROXY_BASE_URL,
- });
-
- // ── Find the profile row and open edit mode ──
- await test.step("open profile in edit mode", async () => {
- const profileRows = page.getByTestId("profile-row");
- const rowCount = await profileRows.count();
- let targetRow: ReturnType | null = null;
-
- for (let i = 0; i < rowCount; i++) {
- const row = profileRows.nth(i);
- const text = await row.textContent();
- if (text?.includes(PROXY_PROFILE)) {
- targetRow = row;
- break;
- }
- }
- expect(
- targetRow,
- `Could not find profile row for "${PROXY_PROFILE}"`,
- ).not.toBeNull();
-
- await targetRow!.getByTestId("profile-menu-trigger").click();
- await waitForTestId(page, "profile-actions-menu");
- await page.getByTestId("profile-edit").click();
-
- // Wait for the editor to load with the profile data
- await expect(page.getByTestId("profile-name-input")).toHaveValue(
- PROXY_PROFILE,
- { timeout: 10_000 },
- );
- });
-
- // ── Ensure we are on the Basic tab, then save ──
- await test.step("switch to Basic view and save", async () => {
- // The litellm_proxy provider + proxy base_url is recognized as a
- // known default, so the form may already be on Basic. Click the
- // toggle to be explicit — this is the same user action the unit
- // test for PR #1148 exercises.
- const basicToggle = page.getByTestId("sdk-section-basic-toggle");
- if (await basicToggle.isVisible().catch(() => false)) {
- await basicToggle.click();
- }
-
- const saveButton = page.getByTestId("save-profile-btn");
- await expect(saveButton).toBeEnabled({ timeout: 10_000 });
- await saveButton.click();
-
- // Wait for save to complete — the UI navigates back to the
- // profile list after a successful save.
- 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 () => {
- const config = await getProfileConfig(request, PROXY_PROFILE);
- assertProxyProfileConfig(
- config,
- LITELLM_PROXY_MODEL,
- OPENHANDS_EQUIVALENT_MODEL,
- OPENHANDS_PROXY_BASE_URL,
- );
- });
-
- // ── Verify: the profile also looks correct after a page reload ──
- await test.step("profile survives page reload", async () => {
- await page.reload({ waitUntil: "domcontentloaded" });
- await waitForTestId(page, "add-llm-profile");
-
- // 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,
- );
- });
- });
-});