Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add runtime size configuration feature #5805

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6b7c15e
Add runtime size configuration feature
openhands-agent Dec 26, 2024
e182008
Trigger lint workflow
openhands-agent Dec 26, 2024
b091c53
Apply lint fixes
openhands-agent Dec 26, 2024
088e308
Fix: Add content to runtime-size-selector.tsx
openhands-agent Dec 26, 2024
1bb6cb3
Merge branch 'main' into feature/runtime-size-configuration
neubig Dec 26, 2024
8323d3e
Merge main into feature/runtime-size-configuration
openhands-agent Dec 29, 2024
40fa281
Fix settings service to handle REMOTE_RUNTIME_RESOURCE_FACTOR
openhands-agent Dec 29, 2024
b79a3a2
Remove embedded git repository
openhands-agent Dec 29, 2024
1d2ee62
Fix linting issues
openhands-agent Dec 29, 2024
3f0ee17
Update runtime size selector style and add SAAS mode check
openhands-agent Dec 29, 2024
60523a3
Add tests for runtime size selector and fix translation key
openhands-agent Dec 29, 2024
9bb0c03
Fix end of file newlines
openhands-agent Dec 29, 2024
8a478e5
Fix accessibility warning in runtime size selector
openhands-agent Dec 29, 2024
7795693
Fix NextUI Select props
openhands-agent Dec 29, 2024
7bf5a18
Fix tests
openhands-agent Dec 29, 2024
2baafc3
Delete .pre-commit-config.yaml
neubig Dec 29, 2024
d5981a3
Remove accidental submodule
openhands-agent Dec 29, 2024
de3ce5e
Merge main and resolve conflicts
openhands-agent Dec 31, 2024
134d5e8
Add remote_runtime_resource_factor to Settings and add tests
openhands-agent Dec 31, 2024
814d490
fix: Add REMOTE_RUNTIME_RESOURCE_FACTOR to settings and fix TypeScrip…
openhands-agent Dec 31, 2024
d465179
fix: Fix linting issues
openhands-agent Dec 31, 2024
2b55b48
fix: Fix settings API and tests
openhands-agent Dec 31, 2024
b675b1f
fix: Fix settings API and tests
openhands-agent Dec 31, 2024
0955a85
fix: Fix settings API and tests
openhands-agent Dec 31, 2024
17f6cb0
Merge branch 'main' into feature/runtime-size-configuration
neubig Jan 1, 2025
33d44f1
Merge branch 'main' into feature/runtime-size-configuration
neubig Jan 2, 2025
8043360
Fix pr #5805: Add runtime size configuration feature
openhands-agent Jan 3, 2025
afc19e6
fix: Replace Tooltip with description in runtime-size-selector.tsx
openhands-agent Jan 3, 2025
bf95273
test: Fix runtime-size-selector tests
openhands-agent Jan 3, 2025
b1f2b2e
test: Make runtime-size-selector tests more resilient
openhands-agent Jan 3, 2025
0dd4f59
Fix formatting issues
openhands-agent Jan 3, 2025
88aa389
test: Fix runtime-size-selector test
openhands-agent Jan 3, 2025
b118fbb
fix: Prevent truncation in runtime size selector description
openhands-agent Jan 3, 2025
ed487f2
fix: Fix TypeScript errors and tests in runtime-size-selector
openhands-agent Jan 3, 2025
43c8d9f
Fix merge conflicts in pyproject.toml
openhands-agent Jan 4, 2025
ff4c5da
Fix pyproject.toml formatting
openhands-agent Jan 4, 2025
24b380f
refactor: remove duplicate settings functions and consolidate setting…
openhands-agent Jan 4, 2025
ab30f25
refactor: move getCurrentSettingsVersion to settings.ts and remove du…
openhands-agent Jan 4, 2025
90435dd
fix: restore getCurrentSettingsVersion to original position
openhands-agent Jan 4, 2025
14f19f1
fix: restore settings.ts to original format
openhands-agent Jan 5, 2025
9785fe5
fix: fix linting issues
openhands-agent Jan 5, 2025
de19418
Fix frontend issues:
openhands-agent Jan 5, 2025
dd69552
Fix TypeScript errors:
openhands-agent Jan 5, 2025
037ebd8
Consolidate settings tests and fix localStorage fallback
openhands-agent Jan 5, 2025
b014c63
Update sandbox config when runtime size setting changes
openhands-agent Jan 5, 2025
601f722
Merge branch 'main' into feature/runtime-size-configuration
neubig Jan 5, 2025
c858fb3
Fix tests
amanape Jan 6, 2025
626a4fb
Remove tests
amanape Jan 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nextProvider } from "react-i18next";
import i18n from "#/i18n";
import { SettingsProvider } from "#/context/settings-context";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { describe, it, expect, vi } from "vitest";
import { MemoryRouter } from "react-router";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { rootReducer } from "#/store";

vi.mock("#/hooks/query/use-config", () => ({
useConfig: () => ({
data: {
APP_MODE: "saas",
},
}),
}));

vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
// This mock makes sure that using the I18nextProvider in the test works
I18nextProvider: ({ children }: { children: React.ReactNode }) => children,
initReactI18next: {
type: "3rdParty",
init: () => {},
},
}));

vi.mock("#/i18n", () => ({
default: {
use: () => ({
init: () => {},
}),
},
}));

const queryClient = new QueryClient();
const store = configureStore({
reducer: rootReducer,
});

const renderSettingsForm = () => {
return render(
<Provider store={store}>
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18n}>
<SettingsProvider>
<SettingsForm
settings={DEFAULT_SETTINGS}
models={[]}
agents={[]}
securityAnalyzers={[]}
onClose={() => {}}
/>
</SettingsProvider>
</I18nextProvider>
</QueryClientProvider>
</MemoryRouter>
</Provider>
);
};

describe("SettingsForm", () => {
it("should not show runtime size selector by default", () => {
renderSettingsForm();
expect(screen.queryByText("Runtime Size")).not.toBeInTheDocument();
});

it("should show runtime size selector when advanced options are enabled", async () => {
renderSettingsForm();
const advancedSwitch = screen.getByRole("switch", {
name: "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL",
});
fireEvent.click(advancedSwitch);
console.log("Advanced switch clicked");
screen.debug();
await screen.findByText("SETTINGS_FORM$RUNTIME_SIZE_LABEL");
});
});
126 changes: 126 additions & 0 deletions frontend/__tests__/services/settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { describe, expect, it, vi, Mock, afterEach } from "vitest";
import {
DEFAULT_SETTINGS,
Settings,
getSettings,
saveSettings,
getLocalStorageSettings,
} from "../../src/services/settings";
import { openHands } from "#/api/open-hands-axios";

vi.mock("#/api/open-hands-axios", () => ({
openHands: {
get: vi.fn(),
post: vi.fn(),
},
}));

Storage.prototype.getItem = vi.fn();
Storage.prototype.setItem = vi.fn();

describe("getSettings", () => {
afterEach(() => {
vi.resetAllMocks();
});

it("should get settings from API", async () => {
const apiSettings = {
llm_model: "llm_value",
llm_base_url: "base_url",
agent: "agent_value",
language: "language_value",
confirmation_mode: true,
security_analyzer: "invariant",
};

(openHands.get as Mock).mockResolvedValueOnce({ data: apiSettings });

const settings = await getSettings();

expect(settings).toEqual({
LLM_MODEL: "llm_value",
LLM_BASE_URL: "base_url",
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "",
CONFIRMATION_MODE: true,
SECURITY_ANALYZER: "invariant",
REMOTE_RUNTIME_RESOURCE_FACTOR: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
});
});

it("should fallback to localStorage if API fails", async () => {
(openHands.get as Mock).mockResolvedValueOnce({ data: null });
(localStorage.getItem as Mock)
.mockReturnValueOnce("llm_value")
.mockReturnValueOnce("base_url")
.mockReturnValueOnce("agent_value")
.mockReturnValueOnce("language_value")
.mockReturnValueOnce("api_key")
.mockReturnValueOnce("true")
.mockReturnValueOnce("invariant");

const settings = await getSettings();

expect(settings).toEqual({
LLM_MODEL: "llm_value",
LLM_BASE_URL: "base_url",
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "api_key",
CONFIRMATION_MODE: true,
SECURITY_ANALYZER: "invariant",
REMOTE_RUNTIME_RESOURCE_FACTOR: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
});
});
});

describe("saveSettings", () => {
it("should save settings to API", async () => {
const settings: Settings = {
LLM_MODEL: "llm_value",
LLM_BASE_URL: "base_url",
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "some_key",
CONFIRMATION_MODE: true,
SECURITY_ANALYZER: "invariant",
REMOTE_RUNTIME_RESOURCE_FACTOR: 2,
};

(openHands.post as Mock).mockResolvedValueOnce({ data: true });

const result = await saveSettings(settings);

expect(result).toBe(true);
expect(openHands.post).toHaveBeenCalledWith("/api/settings", {
llm_model: "llm_value",
llm_base_url: "base_url",
agent: "agent_value",
language: "language_value",
llm_api_key: "some_key",
confirmation_mode: true,
security_analyzer: "invariant",
remote_runtime_resource_factor: 2,
});
});

it("should handle API errors", async () => {
const settings: Settings = {
LLM_MODEL: "llm_value",
LLM_BASE_URL: "base_url",
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "some_key",
CONFIRMATION_MODE: true,
SECURITY_ANALYZER: "invariant",
REMOTE_RUNTIME_RESOURCE_FACTOR: 2,
};

(openHands.post as Mock).mockRejectedValueOnce(new Error("API Error"));

const result = await saveSettings(settings);

expect(result).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function AdvancedOptionSwitch({
<Switch
isDisabled={isDisabled}
name="use-advanced-options"
isSelected={showAdvancedOptions}
defaultSelected={showAdvancedOptions}
onValueChange={setShowAdvancedOptions}
classNames={{
thumb: cn(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Select, SelectItem } from "@nextui-org/react";
import { useConfig } from "#/hooks/query/use-config";

interface RuntimeSizeSelectorProps {
isDisabled: boolean;
defaultValue?: number;
}

export function RuntimeSizeSelector({
isDisabled,
defaultValue,
}: RuntimeSizeSelectorProps) {
const { t } = useTranslation();
const { data: config } = useConfig();
const isSaasMode = config?.APP_MODE === "saas";

if (!isSaasMode) {
return null;
}

return (
<fieldset className="flex flex-col gap-2">
<label
htmlFor="runtime-size"
className="font-[500] text-[#A3A3A3] text-xs"
>
{t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")}
</label>
<Select
id="runtime-size"
name="runtime-size"
defaultSelectedKeys={[String(defaultValue || 1)]}
isDisabled={isDisabled}
aria-label={t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")}
classNames={{
trigger: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}
>
<SelectItem key="1" value={1}>
{t("1x (2 core, 8G)")}
</SelectItem>
<SelectItem key="2" value={2}>
{t("2x (4 core, 16G)")}
</SelectItem>
</Select>
</fieldset>
);
}
23 changes: 15 additions & 8 deletions frontend/src/components/shared/modals/settings/settings-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { SecurityAnalyzerInput } from "../../inputs/security-analyzers-input";
import { ModalBackdrop } from "../modal-backdrop";
import { ModelSelector } from "./model-selector";

import { RuntimeSizeSelector } from "./runtime-size-selector";

interface SettingsFormProps {
disabled?: boolean;
settings: Settings;
Expand Down Expand Up @@ -98,6 +100,8 @@ export function SettingsForm({
posthog.capture("settings_saved", {
LLM_MODEL: newSettings.LLM_MODEL,
LLM_API_KEY: newSettings.LLM_API_KEY ? "SET" : "UNSET",
REMOTE_RUNTIME_RESOURCE_FACTOR:
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
});
};

Expand Down Expand Up @@ -168,16 +172,19 @@ export function SettingsForm({
defaultValue={settings.LLM_API_KEY || ""}
/>

{showAdvancedOptions && (
<AgentInput
isDisabled={!!disabled}
defaultValue={settings.AGENT}
agents={agents}
/>
)}

{showAdvancedOptions && (
<>
<AgentInput
isDisabled={!!disabled}
defaultValue={settings.AGENT}
agents={agents}
/>

<RuntimeSizeSelector
isDisabled={!!disabled}
defaultValue={settings.REMOTE_RUNTIME_RESOURCE_FACTOR}
/>

<SecurityAnalyzerInput
isDisabled={!!disabled}
defaultValue={settings.SECURITY_ANALYZER}
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/services/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Settings = {
LLM_API_KEY: string | null;
CONFIRMATION_MODE: boolean;
SECURITY_ANALYZER: string;
REMOTE_RUNTIME_RESOURCE_FACTOR: number;
};

export type ApiSettings = {
Expand All @@ -20,6 +21,7 @@ export type ApiSettings = {
llm_api_key: string | null;
confirmation_mode: boolean;
security_analyzer: string;
remote_runtime_resource_factor: number;
};

export const DEFAULT_SETTINGS: Settings = {
Expand All @@ -30,6 +32,7 @@ export const DEFAULT_SETTINGS: Settings = {
LLM_API_KEY: null,
CONFIRMATION_MODE: false,
SECURITY_ANALYZER: "",
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
};

export const getCurrentSettingsVersion = () => {
Expand Down Expand Up @@ -63,6 +66,8 @@ export const getLocalStorageSettings = (): Settings => {
LLM_API_KEY: llmApiKey || DEFAULT_SETTINGS.LLM_API_KEY,
CONFIRMATION_MODE: confirmationMode || DEFAULT_SETTINGS.CONFIRMATION_MODE,
SECURITY_ANALYZER: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
REMOTE_RUNTIME_RESOURCE_FACTOR:
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
};
};

Expand All @@ -82,6 +87,8 @@ export const saveSettings = async (
confirmation_mode: settings.CONFIRMATION_MODE || null,
security_analyzer: settings.SECURITY_ANALYZER || null,
llm_api_key: settings.LLM_API_KEY || null,
remote_runtime_resource_factor:
settings.REMOTE_RUNTIME_RESOURCE_FACTOR || null,
};

const { data } = await openHands.post("/api/settings", apiSettings);
Expand Down Expand Up @@ -117,6 +124,8 @@ export const maybeMigrateSettings = async (logout: () => void) => {

if (currentVersion < 5) {
const localSettings = getLocalStorageSettings();
localSettings.REMOTE_RUNTIME_RESOURCE_FACTOR =
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR;
await saveSettings(localSettings);
}
};
Expand All @@ -141,6 +150,8 @@ export const getSettings = async (): Promise<Settings> => {
CONFIRMATION_MODE: apiSettings.confirmation_mode,
SECURITY_ANALYZER: apiSettings.security_analyzer,
LLM_API_KEY: "",
REMOTE_RUNTIME_RESOURCE_FACTOR:
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
};
}
return getLocalStorageSettings();
Expand Down
Loading