feat: fix default and free models (#16922)

Co-authored-by: neubig <neubig@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Juan Pedro Michelini Jorge <juan@juan.com.uy>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
This commit is contained in:
Graham Neubig
2026-08-27 15:58:35 +00:00
committed by GitHub
co-authored by neubig openhands Juan Pedro Michelini Jorge allhands-bot
parent f95b9b71f7
commit d104ffdc33
17 changed files with 76 additions and 88 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ describe("SettingsService", () => {
// Should have normalized settings with derived fields
expect(settings.agent).toBe("CodeActAgent");
expect(settings.llm_model).toBe("openhands/kimi-k3");
expect(settings.llm_model).toBe("openai/gpt-5.6-sol");
expect(settings.confirmation_mode).toBe(false);
expect(settings.security_analyzer).toBe("llm");
});
@@ -101,27 +101,29 @@ describe("ChatInputLlmProfilePicker", () => {
profiles: [
{
name: "Free",
model: "openhands/glm-5.2",
model: "openhands/deepseek-v4-flash",
base_url: null,
api_key_set: true,
},
],
currentProfileName: "Free",
currentProfileModel: "openhands/glm-5.2",
currentProfileModel: "openhands/deepseek-v4-flash",
}),
);
renderWithProviders(<ChatInputLlmProfilePicker />);
fireEvent.click(screen.getByTestId("chat-input-llm-profile"));
expect(screen.getByText("OpenHands GLM-5.2 (free)")).toBeInTheDocument();
expect(
screen.getByText("OpenHands DeepSeek V4 Flash (free)"),
).toBeInTheDocument();
});
it("labels a free OpenHands route in the read-only profile menu", () => {
useChatInputLlmProfileStateMock.mockReturnValue(
state({
canSwitchProfile: false,
currentProfileModel: "openhands/glm-5.2",
currentProfileModel: "openhands/deepseek-v4-flash",
}),
);
@@ -130,7 +132,7 @@ describe("ChatInputLlmProfilePicker", () => {
expect(
screen.getByTestId("chat-input-llm-profile-current"),
).toHaveTextContent("OpenHands GLM-5.2 (free)");
).toHaveTextContent("OpenHands DeepSeek V4 Flash (free)");
});
it("links to the LLM profiles settings page", () => {
@@ -110,7 +110,7 @@ describe("ModelMessages", () => {
useModelStore.getState().show(CONVERSATION_ID, "event-1", [
{
name: "free",
model: "openhands/glm-5.2",
model: "openhands/deepseek-v4-flash",
base_url: null,
api_key_set: true,
},
@@ -129,7 +129,7 @@ describe("ModelMessages", () => {
);
expect(
screen.getByText(/model:\s+OpenHands GLM-5\.2 \(free\)/),
screen.getByText(/model:\s+OpenHands DeepSeek V4 Flash \(free\)/),
).toBeInTheDocument();
});
});
@@ -198,8 +198,12 @@ describe("ConversationCard", () => {
const model = screen.getByTestId("conversation-card-agent-chip");
const tags = screen.getByTestId("conversation-card-tag-chips");
expect(repo.compareDocumentPosition(model) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(model.compareDocumentPosition(tags) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(
repo.compareDocumentPosition(model) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(
model.compareDocumentPosition(tags) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
});
it("renders the workspace folder name when no repository is selected", () => {
@@ -985,10 +989,7 @@ describe("ConversationCard", () => {
const chip = screen.getByTestId("conversation-card-agent-chip");
expect(chip).toHaveTextContent("Claude Opus (1M)");
expect(chip).toHaveAttribute(
"title",
"Claude Code · Claude Opus (1M)",
);
expect(chip).toHaveAttribute("title", "Claude Code · Claude Opus (1M)");
});
it("falls back to the provider display name for an ACP conversation with no model", () => {
@@ -1098,7 +1099,7 @@ describe("ConversationCard", () => {
);
const chip = screen.getByTestId("conversation-card-agent-chip");
expect(chip).toHaveTextContent("OpenHands GLM-5.2 (free)");
expect(chip).toHaveTextContent("glm-5.2");
expect(chip).toHaveAttribute("title", "openhands/glm-5.2");
});
@@ -69,31 +69,25 @@ describe("ModelSelector — OpenHands provider display", () => {
it("makes clear which OpenHands models are free", async () => {
const user = userEvent.setup();
renderWithQuery(<ModelSelector currentModel="openhands/glm-5.2" />);
renderWithQuery(<ModelSelector currentModel="openhands/deepseek-v4-flash" />);
await waitFor(() => {
expect(
screen.getByTestId("openhands-free-models-note"),
).toHaveTextContent("openhands/glm-5.2");
});
expect(screen.getByTestId("openhands-free-models-note")).toHaveTextContent(
"openhands/deepseek-v4-flash",
);
expect(screen.getByTestId("openhands-free-models-note")).toHaveTextContent(
"openhands/minimax-m2.7",
);
expect(screen.getByTestId("selected-free-model-badge")).toHaveTextContent(
"Free",
);
await user.click(screen.getByLabelText("LLM$MODEL"));
expect(screen.getAllByText("Free")).toHaveLength(4);
expect(screen.getByLabelText("LLM$MODEL")).toHaveValue("glm-5.2");
expect(screen.getAllByText("Free")).toHaveLength(2);
expect(screen.getByLabelText("LLM$MODEL")).toHaveValue("deepseek-v4-flash");
await user.click(screen.getByText("minimax-m2.7"));
await user.click(screen.getByRole("option", { name: /deepseek-v4-flash/ }));
expect(screen.getByLabelText("LLM$MODEL")).toHaveValue("minimax-m2.7");
expect(screen.getByLabelText("LLM$MODEL")).toHaveValue("deepseek-v4-flash");
expect(screen.getByTestId("selected-free-model-badge")).toHaveTextContent(
"Free",
);
@@ -344,12 +344,12 @@ describe("LlmSettingsLocalView", () => {
// Should be in create view
expect(screen.getByTestId("profile-name-input")).toBeInTheDocument();
// The profile name is auto-derived from the prefilled free default model.
// The profile name is auto-derived from the prefilled default model.
const nameInput = screen.getByTestId("profile-name-input");
expect(nameInput).toHaveValue("kimi-k3");
expect(nameInput).toHaveValue("gpt-5.6-sol");
expect(screen.getByTestId("mock-basic-model-input")).toHaveValue(
"openhands/kimi-k3",
"openai/gpt-5.6-sol",
);
});
@@ -363,7 +363,7 @@ describe("LlmSettingsLocalView", () => {
// The profile name starts from the free-model derived default.
const nameInput = screen.getByTestId("profile-name-input");
expect(nameInput).toHaveValue("kimi-k3");
expect(nameInput).toHaveValue("gpt-5.6-sol");
// Go back to list
await user.click(screen.getByTestId("back-to-profiles"));
@@ -375,7 +375,7 @@ describe("LlmSettingsLocalView", () => {
// The profile name should return to the free-model derived default
// again (fresh form).
const freshNameInput = screen.getByTestId("profile-name-input");
expect(freshNameInput).toHaveValue("kimi-k3");
expect(freshNameInput).toHaveValue("gpt-5.6-sol");
});
it("does not carry over values from edit mode to create mode", async () => {
@@ -390,7 +390,7 @@ describe("LlmSettingsLocalView", () => {
// Should be in create view with the free-model derived profile name.
const nameInput = screen.getByTestId("profile-name-input");
expect(nameInput).toHaveValue("kimi-k3");
expect(nameInput).toHaveValue("gpt-5.6-sol");
// The key "new-profile" should be used, ensuring a fresh form mount
// that doesn't inherit any existing profile data
@@ -56,14 +56,13 @@ describe("ProfileRow", () => {
render(
<ProfileRow
{...defaultProps}
profile={{ ...mockProfile, model: "openhands/glm-5.2" }}
profile={{ ...mockProfile, model: "openhands/deepseek-v4-flash" }}
/>,
);
expect(screen.getByText("OpenHands GLM-5.2 (free)")).toHaveAttribute(
"title",
"openhands/glm-5.2",
);
expect(
screen.getByText("OpenHands DeepSeek V4 Flash (free)"),
).toHaveAttribute("title", "openhands/deepseek-v4-flash");
});
it("does not display model when null", () => {
@@ -156,7 +156,7 @@ describe("useSlashCommand", () => {
},
{
name: "free",
model: "openhands/glm-5.2",
model: "openhands/deepseek-v4-flash",
base_url: null,
api_key_set: true,
},
@@ -180,7 +180,7 @@ describe("useSlashCommand", () => {
expect(
result.current.filteredItems.find((i) => i.command === "/model free")
?.skill.content,
).toBe("Switch to OpenHands GLM-5.2 (free)");
).toBe("Switch to OpenHands DeepSeek V4 Flash (free)");
});
it("filters saved LLM profile suggestions by profile name or model", () => {
+1 -6
View File
@@ -17,10 +17,7 @@ describe("formatNativeModelName", () => {
it("labels only configured OpenHands free-model routes as free", () => {
expect(Object.keys(FREE_OPENHANDS_MODELS)).toEqual([
"openhands/kimi-k3",
"openhands/glm-5.2",
"openhands/deepseek-v4-flash",
"openhands/minimax-m2.7",
]);
for (const [model, label] of Object.entries(FREE_OPENHANDS_MODELS)) {
@@ -39,9 +36,7 @@ describe("formatNativeModelName", () => {
});
it("keeps free OpenHands labels on native conversation chips", () => {
expect(formatNativeModelName("openhands/glm-5.2")).toBe(
FREE_OPENHANDS_MODELS["openhands/glm-5.2"],
);
expect(formatNativeModelName("openhands/glm-5.2")).toBe("glm-5.2");
expect(formatNativeModelName("openhands/deepseek-v4-flash")).toBe(
FREE_OPENHANDS_MODELS["openhands/deepseek-v4-flash"],
);
+2 -1
View File
@@ -3,7 +3,8 @@
---
### LLD-001: Frontend always sends its chosen default model
- [x] When the agent-server returns an absent or empty `llm.model` (e.g. because the user has never saved settings), the frontend adapter shall substitute `DEFAULT_SETTINGS.llm_model` (`"openhands/kimi-k3"`) before sending the conversation-start request.
- [x] When the agent-server returns an absent or empty `llm.model` (e.g. because the user has never saved settings), the frontend adapter shall substitute `DEFAULT_SETTINGS.llm_model` (`"openai/gpt-5.6-sol"`) before sending the conversation-start request.
- [x] The frontend shall never rely on the agent-server SDK's own default model (`gpt-5.5`); it shall always send an explicit model value.
- [x] Whitespace-only model strings shall be treated as absent and fall back to the default.
- [x] The same guard applies when LLM settings arrive via `encryptedAgentSettings` (the conversation-start encrypted payload path).
@@ -16,16 +16,12 @@ interface SetupLlmStepProps {
}
/**
* Pre-fills the LLM form with the OpenHands provider's free default model
* (`openhands/kimi-k3`), matching `DEFAULT_SETTINGS.llm_model`. The OpenHands
* provider is the agent the user just selected, so the onboarding override
* keeps the LLM provider aligned with that choice rather than silently
* switching to OpenAI. Canvas stores provider-qualified LiteLLM model ids,
* so the override uses this OpenHands-prefixed model id, and keeping it as an
* explicit override marks the model dirty so the Next button persists the
* suggested default immediately.
* Pre-fills the LLM form with the OpenAI GPT-5.6 Sol default
* (`openai/gpt-5.6-sol`), matching `DEFAULT_SETTINGS.llm_model`. The explicit
* override marks the model dirty so the Next button persists the suggested
* default immediately.
*/
export const ONBOARDING_DEFAULT_LLM_MODEL = "openhands/kimi-k3";
export const ONBOARDING_DEFAULT_LLM_MODEL = "openai/gpt-5.6-sol";
/**
* Step 2: embed the LLM settings form. The screen runs in `embedded`
+15 -15
View File
@@ -30583,21 +30583,21 @@
"uk": "Налаштуйте свій LLM"
},
"ONBOARDING$LLM_SUBTITLE": {
"en": "Pick a provider and model. We've pre-selected OpenHands GLM-5.2.",
"ja": "プロバイダーとモデルを選択してください。OpenHands GLM-5.2 を事前に選択しています。",
"zh-CN": "选择提供商和模型。我们已为你预先选择 OpenHands GLM-5.2。",
"zh-TW": "選擇供應商與模型。我們已預先選好 OpenHands GLM-5.2。",
"ko-KR": "공급자와 모델을 선택하세요. 기본값으로 OpenHands GLM-5.2가 선택되어 있습니다.",
"no": "Velg en leverandør og modell. Vi har forhåndsvalgt OpenHands GLM-5.2.",
"ar": "اختر مزوّدًا ونموذجًا. لقد اخترنا مسبقًا OpenHands GLM-5.2.",
"de": "Wählen Sie Anbieter und Modell. Wir haben OpenHands GLM-5.2 vorausgewählt.",
"fr": "Choisissez un fournisseur et un modèle. Nous avons présélectionné OpenHands GLM-5.2.",
"it": "Scegli un provider e un modello. Abbiamo preselezionato OpenHands GLM-5.2.",
"pt": "Escolha um provedor e modelo. Já selecionamos o OpenHands GLM-5.2.",
"es": "Elige un proveedor y modelo. Hemos preseleccionado OpenHands GLM-5.2.",
"ca": "Trieu un proveïdor i model. Hem preseleccionat OpenHands GLM-5.2.",
"tr": "Bir sağlayıcı ve model seçin. OpenHands GLM-5.2'i önceden seçtik.",
"uk": "Виберіть провайдера та модель. OpenHands GLM-5.2 уже обрано."
"en": "Pick a provider and model. We've pre-selected OpenAI GPT-5.6 Sol.",
"ja": "プロバイダーとモデルを選択してください。OpenAI GPT-5.6 Sol を事前に選択しています。",
"zh-CN": "选择提供商和模型。我们已为你预先选择 OpenAI GPT-5.6 Sol。",
"zh-TW": "選擇供應商與模型。我們已預先選好 OpenAI GPT-5.6 Sol。",
"ko-KR": "공급자와 모델을 선택하세요. 기본값으로 OpenAI GPT-5.6 Sol가 선택되어 있습니다.",
"no": "Velg en leverandør og modell. Vi har forhåndsvalgt OpenAI GPT-5.6 Sol.",
"ar": "اختر مزوّدًا ونموذجًا. لقد اخترنا مسبقًا OpenAI GPT-5.6 Sol.",
"de": "Wählen Sie Anbieter und Modell. Wir haben OpenAI GPT-5.6 Sol vorausgewählt.",
"fr": "Choisissez un fournisseur et un modèle. Nous avons présélectionné OpenAI GPT-5.6 Sol.",
"it": "Scegli un provider e un modello. Abbiamo preselezionato OpenAI GPT-5.6 Sol.",
"pt": "Escolha um provedor e modelo. Já selecionamos o OpenAI GPT-5.6 Sol.",
"es": "Elige un proveedor y modelo. Hemos preseleccionado OpenAI GPT-5.6 Sol.",
"ca": "Trieu un proveïdor i model. Hem preseleccionat OpenAI GPT-5.6 Sol.",
"tr": "Bir sağlayıcı ve model seçin. OpenAI GPT-5.6 Sol'i önceden seçtik.",
"uk": "Виберіть провайдера та модель. OpenAI GPT-5.6 Sol уже обрано."
},
"ONBOARDING$HELLO_TITLE": {
"en": "Say hello 👋",
+5 -3
View File
@@ -610,7 +610,8 @@ const MOCK_MODELS = [
"openhands/claude-sonnet-4-5-20250929",
"openhands/claude-haiku-4-5-20251001",
"openhands/claude-opus-4-5-20251101",
"openhands/kimi-k3",
"openai/gpt-5.6-sol",
"openhands/deepseek-v4-flash",
"openhands/glm-5.2",
"sambanova/Meta-Llama-3.1-8B-Instruct",
];
@@ -624,7 +625,8 @@ const MOCK_VERIFIED_MODELS = new Set([
"openai/gpt-5.5",
"openhands/claude-opus-4-5-20251101",
"openhands/claude-sonnet-4-5-20250929",
"openhands/kimi-k3",
"openai/gpt-5.6-sol",
"openhands/deepseek-v4-flash",
"openhands/glm-5.2",
]);
@@ -755,7 +757,7 @@ export const SETTINGS_HANDLERS = [
"claude-sonnet-4-5-20250929",
],
verified_providers: MOCK_VERIFIED_PROVIDERS,
default_model: "openhands/kimi-k3",
default_model: "openai/gpt-5.6-sol",
}),
),
+2 -2
View File
@@ -3,7 +3,7 @@ import { Settings } from "#/types/settings";
export const LATEST_SETTINGS_VERSION = 5;
export const DEFAULT_SETTINGS: Settings = {
llm_model: "openhands/kimi-k3",
llm_model: "openai/gpt-5.6-sol",
llm_base_url: "",
agent: "CodeActAgent",
language: "en",
@@ -37,7 +37,7 @@ export const DEFAULT_SETTINGS: Settings = {
agent_kind: "openhands",
agent: "CodeActAgent",
llm: {
model: "openhands/kimi-k3",
model: "openai/gpt-5.6-sol",
},
condenser: {
enabled: true,
-3
View File
@@ -1,10 +1,7 @@
export const FREE_MODEL_BADGE_LABEL = "Free";
export const FREE_OPENHANDS_MODELS = {
"openhands/kimi-k3": "OpenHands Kimi K3 (free)",
"openhands/glm-5.2": "OpenHands GLM-5.2 (free)",
"openhands/deepseek-v4-flash": "OpenHands DeepSeek V4 Flash (free)",
"openhands/minimax-m2.7": "OpenHands MiniMax M2.7 (free)",
} as const;
export const FREE_OPENHANDS_MODEL_IDS = Object.keys(FREE_OPENHANDS_MODELS);
@@ -61,10 +61,11 @@ test.describe("onboarding recent regressions", () => {
});
// Regression coverage for #1077 / PR #1089: first-run LLM setup
// should default users to the OpenHands provider, matching the agent
// selected on the previous step.
// should default users to the configured default provider and model.
test("defaults the LLM setup step to OpenHands GLM-5.2", async ({ page }) => {
test("defaults the LLM setup step to OpenAI GPT-5.6 Sol", async ({
page,
}) => {
await showOnboarding(page, {
apiKey: SESSION_API_KEY,
beforeGoto: async () => {
@@ -94,18 +95,18 @@ test.describe("onboarding recent regressions", () => {
await expect(
providerInput,
"first-run onboarding should default to the OpenHands provider",
).toHaveValue("OpenHands", { timeout: 10_000 });
"first-run onboarding should default to the OpenAI provider",
).toHaveValue("OpenAI", { timeout: 10_000 });
// The model input displays the model ID without the provider prefix.
await expect(
modelInput,
"first-run onboarding should default to Kimi K3",
).toHaveValue("kimi-k3", {
"first-run onboarding should default to GPT-5.6 Sol",
).toHaveValue("gpt-5.6-sol", {
timeout: 10_000,
});
await expect(
page.getByTestId("openhands-account-help"),
"OpenHands account helper should be visible for OpenHands defaults",
).toBeVisible();
"OpenHands account helper should be hidden for OpenAI defaults",
).toBeHidden();
});
});
+1 -1
View File
@@ -36,7 +36,7 @@ export async function routeOnboardingLlmCatalog(page: Page) {
"anthropic/claude-opus-4-8",
"openai/gpt-5.5",
"openhands/claude-opus-4-5-20251101",
"openhands/kimi-k3",
"openai/gpt-5.6-sol",
"openhands/glm-5.2",
],
}),