diff --git a/frontend/__tests__/utils/model-name-case-preservation.test.tsx b/frontend/__tests__/utils/model-name-case-preservation.test.tsx new file mode 100644 index 0000000000..4af08e127f --- /dev/null +++ b/frontend/__tests__/utils/model-name-case-preservation.test.tsx @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { extractSettings } from "#/utils/settings-utils"; + +describe("Model name case preservation", () => { + it("should preserve the original case of model names in extractSettings", () => { + // Create FormData with proper casing + const formData = new FormData(); + formData.set("llm-provider-input", "SambaNova"); + formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct"); + formData.set("agent", "CodeActAgent"); + formData.set("language", "en"); + + const settings = extractSettings(formData); + + // Test that model names maintain their original casing + expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct"); + }); + + it("should preserve openai model case", () => { + const formData = new FormData(); + formData.set("llm-provider-input", "openai"); + formData.set("llm-model-input", "gpt-4o"); + formData.set("agent", "CodeActAgent"); + formData.set("language", "en"); + + const settings = extractSettings(formData); + expect(settings.LLM_MODEL).toBe("openai/gpt-4o"); + }); + + it("should preserve anthropic model case", () => { + const formData = new FormData(); + formData.set("llm-provider-input", "anthropic"); + formData.set("llm-model-input", "claude-sonnet-4-20250514"); + formData.set("agent", "CodeActAgent"); + formData.set("language", "en"); + + const settings = extractSettings(formData); + expect(settings.LLM_MODEL).toBe("anthropic/claude-sonnet-4-20250514"); + }); + + it("should not automatically lowercase model names", () => { + const formData = new FormData(); + formData.set("llm-provider-input", "SambaNova"); + formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct"); + formData.set("agent", "CodeActAgent"); + formData.set("language", "en"); + + const settings = extractSettings(formData); + + // Test that camelCase and PascalCase are preserved + expect(settings.LLM_MODEL).not.toBe("sambanova/meta-llama-3.1-8b-instruct"); + expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct"); + }); +}); diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 928f19c754..cd9dfeb2c2 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -111,6 +111,7 @@ const openHandsHandlers = [ "gpt-4o-mini", "anthropic/claude-3.5", "anthropic/claude-sonnet-4-20250514", + "sambanova/Meta-Llama-3.1-8B-Instruct", ]), ), diff --git a/frontend/src/routes/llm-settings.tsx b/frontend/src/routes/llm-settings.tsx index 8a8d6bb412..0cdfa32feb 100644 --- a/frontend/src/routes/llm-settings.tsx +++ b/frontend/src/routes/llm-settings.tsx @@ -23,6 +23,7 @@ import { isCustomModel } from "#/utils/is-custom-model"; import { LlmSettingsInputsSkeleton } from "#/components/features/settings/llm-settings/llm-settings-inputs-skeleton"; import { KeyStatusIcon } from "#/components/features/settings/key-status-icon"; import { DEFAULT_SETTINGS } from "#/services/settings"; +import { getProviderId } from "#/utils/map-provider"; function LlmSettingsScreen() { const { t } = useTranslation(); @@ -93,13 +94,15 @@ function LlmSettingsScreen() { }; const basicFormAction = (formData: FormData) => { - const provider = formData.get("llm-provider-input")?.toString(); + const providerDisplay = formData.get("llm-provider-input")?.toString(); + const provider = providerDisplay + ? getProviderId(providerDisplay) + : undefined; const model = formData.get("llm-model-input")?.toString(); const apiKey = formData.get("llm-api-key-input")?.toString(); const searchApiKey = formData.get("search-api-key-input")?.toString(); - const fullLlmModel = - provider && model && `${provider}/${model}`.toLowerCase(); + const fullLlmModel = provider && model && `${provider}/${model}`; saveSettings( { diff --git a/frontend/src/utils/__tests__/settings-utils.test.ts b/frontend/src/utils/__tests__/settings-utils.test.ts index 130cbbe555..bebdaa0f88 100644 --- a/frontend/src/utils/__tests__/settings-utils.test.ts +++ b/frontend/src/utils/__tests__/settings-utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { parseMaxBudgetPerTask } from "../settings-utils"; +import { parseMaxBudgetPerTask, extractSettings } from "../settings-utils"; describe("parseMaxBudgetPerTask", () => { it("should return null for empty string", () => { @@ -47,3 +47,45 @@ describe("parseMaxBudgetPerTask", () => { expect(parseMaxBudgetPerTask("5e-1")).toBeNull(); // 0.5, which is < 1 }); }); + +describe("extractSettings", () => { + it("should preserve model name case when extracting settings", () => { + // Test cases with various model name formats + const testCases = [ + { provider: "sambanova", model: "Meta-Llama-3.1-8B-Instruct" }, + { provider: "openai", model: "GPT-4o" }, + { provider: "anthropic", model: "Claude-3-5-Sonnet" }, + { provider: "openrouter", model: "CamelCaseModel" }, + ]; + + testCases.forEach(({ provider, model }) => { + const formData = new FormData(); + formData.set("llm-provider-input", provider); + formData.set("llm-model-input", model); + + const settings = extractSettings(formData); + + // Verify that the model name case is preserved + const expectedModel = `${provider}/${model}`; + expect(settings.LLM_MODEL).toBe(expectedModel); + // Only test that it's not lowercased if the original has uppercase letters + if (expectedModel !== expectedModel.toLowerCase()) { + expect(settings.LLM_MODEL).not.toBe(expectedModel.toLowerCase()); + } + }); + }); + + it("should handle custom model without lowercasing", () => { + const formData = new FormData(); + formData.set("llm-provider-input", "sambanova"); + formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct"); + formData.set("use-advanced-options", "true"); + formData.set("custom-model", "Custom-Model-Name"); + + const settings = extractSettings(formData); + + // Custom model should take precedence and preserve case + expect(settings.LLM_MODEL).toBe("Custom-Model-Name"); + expect(settings.LLM_MODEL).not.toBe("custom-model-name"); + }); +}); diff --git a/frontend/src/utils/map-provider.ts b/frontend/src/utils/map-provider.ts index 1600d10e2d..efa339e114 100644 --- a/frontend/src/utils/map-provider.ts +++ b/frontend/src/utils/map-provider.ts @@ -29,3 +29,10 @@ export const mapProvider = (provider: string) => Object.keys(MAP_PROVIDER).includes(provider) ? MAP_PROVIDER[provider as keyof typeof MAP_PROVIDER] : provider; + +export const getProviderId = (displayName: string): string => { + const entry = Object.entries(MAP_PROVIDER).find( + ([, value]) => value === displayName, + ); + return entry ? entry[0] : displayName; +}; diff --git a/frontend/src/utils/settings-utils.ts b/frontend/src/utils/settings-utils.ts index 93c5a5d4b1..ca56b25170 100644 --- a/frontend/src/utils/settings-utils.ts +++ b/frontend/src/utils/settings-utils.ts @@ -1,10 +1,12 @@ import { Settings } from "#/types/settings"; +import { getProviderId } from "#/utils/map-provider"; const extractBasicFormData = (formData: FormData) => { - const provider = formData.get("llm-provider-input")?.toString(); + const providerDisplay = formData.get("llm-provider-input")?.toString(); + const provider = providerDisplay ? getProviderId(providerDisplay) : undefined; const model = formData.get("llm-model-input")?.toString(); - const LLM_MODEL = `${provider}/${model}`.toLowerCase(); + const LLM_MODEL = `${provider}/${model}`; const LLM_API_KEY = formData.get("llm-api-key-input")?.toString(); const AGENT = formData.get("agent")?.toString(); const LANGUAGE = formData.get("language")?.toString();