mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
Fix automatic lowercasing of model names in LLM integration (#9271)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
]),
|
||||
),
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user