Fix automatic lowercasing of model names in LLM integration (#9271)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig
2025-06-23 14:59:06 -04:00
committed by GitHub
parent 5e5168ffd4
commit c29b5e9757
6 changed files with 115 additions and 6 deletions

View File

@@ -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");
});
});

View File

@@ -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",
]),
),

View File

@@ -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(
{

View File

@@ -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");
});
});

View File

@@ -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;
};

View File

@@ -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();