Repository: All-Hands-AI/OpenHands
Pull request: Send initialization arguments only when stored in localStorage
Fixes https://github.com/OpenDevin/OpenDevin/issues/617 (parent https://github.com/OpenDevin/OpenDevin/issues/603)

Settings will only be saved to localStorage when the settings are explicitly changed by the user. Otherwise, the initialization arguments are left blank to respect the server's defaults.

Problem statement:
Send `LLM_MODEL` to frontend
The `LLM_MODEL` setting defines the default model to use when no other model is selected in the frontend.

When first initialized, the frontend needs a model selected in the settings dialog. Currently this is hardcoded to `gpt-4-0125-preview`. Since the frontend now sends this model as an initialization argument to the backend, the `LLM_MODEL` setting is effectively not respected.

To fix, the backend should send `LLM_MODEL` to the frontend. If there is no `model` key in localStorage, it should be initialized with the value of `LLM_MODEL`.

Hints from issue discussion:
How does the backend send the value of `LLM_MODEL` to the frontend? Are you thinking that's a new endpoint?
Yes.

Commit message: Send initialization arguments only when stored in localStorage

Patch to review:
diff --git a/frontend/.eslintrc b/frontend/.eslintrc
index e259190dc563..c151040b0efd 100644
--- a/frontend/.eslintrc
+++ b/frontend/.eslintrc
@@ -18,11 +18,11 @@
     {
       "files": ["*.ts", "*.tsx"],
       "rules": {
-        // Allow state modification in Redux reducers
+        // Allow state modification in reduce and Redux reducers
         "no-param-reassign": ["error", {
           "props": true,
           "ignorePropertyModificationsFor": [
-            "state"
+            "acc", "state"
           ]
         }],
         // For https://stackoverflow.com/questions/55844608/stuck-with-eslint-error-i-e-separately-loops-should-be-avoided-in-favor-of-arra
diff --git a/frontend/src/components/SettingModal.tsx b/frontend/src/components/SettingModal.tsx
index fdac5880eb51..b95be897731a 100644
--- a/frontend/src/components/SettingModal.tsx
+++ b/frontend/src/components/SettingModal.tsx
@@ -18,6 +18,7 @@ import {
   fetchAgents,
   INITIAL_MODELS,
   sendSettings,
+  getInitialModel,
 } from "../services/settingsService";
 import {
   setModel,
@@ -54,6 +55,12 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
   );
 
   useEffect(() => {
+    async function setInitialModel() {
+      const initialModel = await getInitialModel();
+      store.dispatch(setModel(initialModel));
+    }
+    setInitialModel();
+
     fetchModels().then((fetchedModels) => {
       setSupportedModels(fetchedModels);
       localStorage.setItem("supportedModels", JSON.stringify(fetchedModels));
@@ -100,7 +107,7 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
               }))}
               label="Model"
               placeholder="Select a model"
-              defaultSelectedKey={model}
+              selectedKey={model}
               // className="max-w-xs"
               onSelectionChange={(key) => {
                 store.dispatch(setModel(key as string));
diff --git a/frontend/src/services/settingsService.ts b/frontend/src/services/settingsService.ts
index 3ff8d1be6da1..51f3e105ff67 100644
--- a/frontend/src/services/settingsService.ts
+++ b/frontend/src/services/settingsService.ts
@@ -2,6 +2,15 @@ import { appendAssistantMessage } from "../state/chatSlice";
 import { setInitialized } from "../state/taskSlice";
 import store from "../store";
 
+export async function getInitialModel() {
+  if (localStorage.getItem("model")) {
+    return localStorage.getItem("model");
+  }
+
+  const res = await fetch("/api/default-model");
+  return res.json();
+}
+
 export async function fetchModels() {
   const response = await fetch(`/api/litellm-models`);
   return response.json();
diff --git a/frontend/src/socket/socket.ts b/frontend/src/socket/socket.ts
index 5284a4fc7f00..b53590fe7aa6 100644
--- a/frontend/src/socket/socket.ts
+++ b/frontend/src/socket/socket.ts
@@ -12,7 +12,17 @@ const WS_URL = `ws://${window.location.host}/ws`;
 const socket = new WebSocket(WS_URL);
 
 socket.addEventListener("open", () => {
-  const { settings } = store.getState();
+  const settingKeys = ["model", "agent", "workspaceDirectory"];
+  const settings = settingKeys.reduce(
+    (acc, key) => {
+      const value = localStorage.getItem(key);
+      if (value) {
+        acc[key] = value;
+      }
+      return acc;
+    },
+    {} as Record<string, string>,
+  );
   sendSettings(socket, settings, false);
 });
 socket.addEventListener("message", (event) => {
diff --git a/frontend/src/state/settingsSlice.ts b/frontend/src/state/settingsSlice.ts
index c24b92daf66c..a694099a6b90 100644
--- a/frontend/src/state/settingsSlice.ts
+++ b/frontend/src/state/settingsSlice.ts
@@ -3,7 +3,7 @@ import { createSlice } from "@reduxjs/toolkit";
 export const settingsSlice = createSlice({
   name: "settings",
   initialState: {
-    model: localStorage.getItem("model") || "gpt-4-0125-preview",
+    model: localStorage.getItem("model") || "",
     agent: localStorage.getItem("agent") || "MonologueAgent",
     workspaceDirectory:
       localStorage.getItem("workspaceDirectory") || "./workspace",
diff --git a/opendevin/mock/listen.py b/opendevin/mock/listen.py
index 20dd3e48e263..ad8c17d414ea 100644
--- a/opendevin/mock/listen.py
+++ b/opendevin/mock/listen.py
@@ -42,5 +42,9 @@ def read_llm_agents():
         "PlannerAgent",
     ]
 
+@app.get("default-model")
+def read_default_model():
+    return "gpt-4"
+
 if __name__ == "__main__":
     uvicorn.run(app, host="127.0.0.1", port=3000)
diff --git a/opendevin/server/listen.py b/opendevin/server/listen.py
index 228701984034..c9d11621fa30 100644
--- a/opendevin/server/listen.py
+++ b/opendevin/server/listen.py
@@ -4,6 +4,7 @@
 import agenthub # noqa F401 (we import this to get the agents registered)
 import litellm 
 from opendevin.agent import Agent
+from opendevin import config
 
 app = FastAPI()
 
@@ -36,3 +37,7 @@ async def get_litellm_agents():
     Get all agents supported by LiteLLM.
     """
     return Agent.listAgents()
+
+@app.get("/default-model")
+def read_default_model():
+    return config.get_or_error("LLM_MODEL")


Code review comments:
diff_hunk: @@ -42,5 +42,9 @@ def read_llm_agents():
         "PlannerAgent",
     ]
 
+@app.get("default-model")
path: opendevin/mock/listen.py
text: @user1:
shouldn't there be a slash at that start of this string?

@author:
Yes, my bad.