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, - ); - }); - }); -});