Skip to content

Commit feae51f

Browse files
committed
Enhance ChatSection and SettingsSection with Lemonade host configuration and model handling improvements.
1 parent aa1761c commit feae51f

File tree

3 files changed

+198
-70
lines changed

3 files changed

+198
-70
lines changed

ee/ui-component/components/chat/ChatSection.tsx

Lines changed: 173 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,49 @@ interface ChatHistoryAPIItem {
4747
};
4848
}
4949

50+
const LEMONADE_DEFAULT_HOSTS = {
51+
direct: "localhost",
52+
docker: "host.docker.internal",
53+
} as const;
54+
55+
type LemonadeHostOption = keyof typeof LEMONADE_DEFAULT_HOSTS;
56+
57+
const asRecord = (value: unknown): Record<string, unknown> | null => {
58+
if (value && typeof value === "object" && !Array.isArray(value)) {
59+
return value as Record<string, unknown>;
60+
}
61+
return null;
62+
};
63+
64+
const asString = (value: unknown): string | undefined => {
65+
if (typeof value === "string") {
66+
const trimmed = value.trim();
67+
return trimmed.length > 0 ? trimmed : undefined;
68+
}
69+
return undefined;
70+
};
71+
72+
const asPortString = (value: unknown): string | undefined => {
73+
if (typeof value === "number" && Number.isFinite(value)) {
74+
return String(value);
75+
}
76+
return asString(value);
77+
};
78+
79+
const fromRecord = (record: Record<string, unknown> | null, key: string): unknown => {
80+
if (!record) return undefined;
81+
return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
82+
};
83+
84+
type AvailableModel = {
85+
id: string;
86+
name: string;
87+
provider: string;
88+
description?: string;
89+
enabled?: boolean;
90+
config?: Record<string, unknown>;
91+
};
92+
5093
/**
5194
* ChatSection component using Vercel-style UI
5295
*/
@@ -172,15 +215,7 @@ const ChatSection: React.FC<ChatSectionProps> = ({
172215
>([]);
173216

174217
const [showModelSelector, setShowModelSelector] = useState(false);
175-
const [availableModels, setAvailableModels] = useState<
176-
Array<{
177-
id: string;
178-
name: string;
179-
provider: string;
180-
description?: string;
181-
enabled?: boolean;
182-
}>
183-
>([]);
218+
const [availableModels, setAvailableModels] = useState<AvailableModel[]>([]);
184219

185220
// Provider configuration is derived on demand; no need to store separately
186221

@@ -533,73 +568,135 @@ const ChatSection: React.FC<ChatSectionProps> = ({
533568
const handleModelChange = (modelId: string) => {
534569
setSelectedModel(modelId);
535570

536-
// Handle default model - clear llm_config to use server default
537571
if (modelId === "default") {
538572
safeUpdateOption("llm_config", undefined);
539573
return;
540574
}
541575

542-
// Check if this is a custom model
576+
const apiKeysRaw = typeof window !== "undefined" ? localStorage.getItem("morphik_api_keys") : null;
577+
let parsedApiKeys: Record<string, unknown> | null = null;
578+
if (apiKeysRaw) {
579+
try {
580+
parsedApiKeys = JSON.parse(apiKeysRaw) as Record<string, unknown>;
581+
} catch (err) {
582+
console.error("Failed to parse API key configuration:", err);
583+
}
584+
}
585+
586+
const lemonadeSettings = asRecord(fromRecord(parsedApiKeys, "lemonade"));
587+
588+
const applyLemonadeOverrides = (configRecord: Record<string, unknown>) => {
589+
const metadata = asRecord(fromRecord(configRecord, "lemonade_metadata"));
590+
const apiBases = asRecord(metadata ? fromRecord(metadata, "api_bases") : undefined);
591+
592+
const hostModeValue =
593+
asString(fromRecord(lemonadeSettings, "hostMode")) ||
594+
asString(fromRecord(lemonadeSettings, "host_mode")) ||
595+
asString(fromRecord(metadata, "host_mode"));
596+
597+
const hostMode: LemonadeHostOption = hostModeValue === "docker" ? "docker" : "direct";
598+
599+
const resolvedPort =
600+
asPortString(fromRecord(lemonadeSettings, "port")) ||
601+
asPortString(fromRecord(lemonadeSettings, "lemonade_port")) ||
602+
asPortString(fromRecord(metadata, "port")) ||
603+
asPortString(fromRecord(metadata, "lemonade_port"));
604+
605+
let resolvedHost =
606+
asString(fromRecord(lemonadeSettings, "host")) || asString(fromRecord(metadata, "backend_host"));
607+
608+
if (!resolvedHost) {
609+
resolvedHost = LEMONADE_DEFAULT_HOSTS[hostMode];
610+
}
611+
612+
let resolvedApiBase =
613+
(hostMode === "docker" ? asString(fromRecord(apiBases, "docker")) : asString(fromRecord(apiBases, "direct"))) ||
614+
asString(fromRecord(apiBases, "selected"));
615+
616+
if (resolvedHost && resolvedPort) {
617+
resolvedApiBase = `http://${resolvedHost}:${resolvedPort}/api/v1`;
618+
}
619+
620+
if (resolvedApiBase) {
621+
configRecord["api_base"] = resolvedApiBase;
622+
}
623+
624+
delete configRecord["lemonade_metadata"];
625+
};
626+
543627
if (modelId.startsWith("custom_")) {
544-
const savedModels = localStorage.getItem("morphik_custom_models");
628+
const savedModels = typeof window !== "undefined" ? localStorage.getItem("morphik_custom_models") : null;
545629
if (savedModels) {
546630
try {
547631
const customModels = JSON.parse(savedModels);
548632
const customModel = customModels.find((m: { id: string }) => `custom_${m.id}` === modelId);
549633

550634
if (customModel) {
551-
// Use the custom model's config directly
552-
safeUpdateOption("llm_config", customModel.config);
635+
const llmConfig: Record<string, unknown> = {
636+
...(customModel.config as Record<string, unknown>),
637+
};
638+
639+
if (customModel.provider === "lemonade") {
640+
applyLemonadeOverrides(llmConfig);
641+
}
642+
643+
safeUpdateOption("llm_config", llmConfig);
553644
return;
554645
}
555646
} catch (err) {
556647
console.error("Failed to parse custom models:", err);
557648
}
558649
}
559-
}
560650

561-
// Get API keys from localStorage
562-
const savedConfig = localStorage.getItem("morphik_api_keys");
563-
if (savedConfig) {
564-
try {
565-
const config = JSON.parse(savedConfig);
651+
const fallbackModel = availableModels.find(model => model.id === modelId);
652+
if (fallbackModel?.config) {
653+
const fallbackConfig = { ...(fallbackModel.config as Record<string, unknown>) };
654+
if (fallbackModel.provider === "lemonade") {
655+
applyLemonadeOverrides(fallbackConfig);
656+
}
657+
safeUpdateOption("llm_config", fallbackConfig);
658+
return;
659+
}
660+
}
566661

567-
// Build model_config based on selected model and saved API keys
568-
const modelConfig: Record<string, unknown> = { model: modelId };
662+
if (parsedApiKeys) {
663+
const providerConfig = parsedApiKeys as Record<string, { apiKey?: string; baseUrl?: string }>;
664+
const modelConfig: Record<string, unknown> = { model: modelId };
569665

570-
// Determine provider from model ID
571-
if (modelId.startsWith("gpt")) {
572-
if (config.openai?.apiKey) {
573-
modelConfig.api_key = config.openai.apiKey;
574-
if (config.openai.baseUrl) {
575-
modelConfig.base_url = config.openai.baseUrl;
576-
}
577-
}
578-
} else if (modelId.startsWith("claude")) {
579-
if (config.anthropic?.apiKey) {
580-
modelConfig.api_key = config.anthropic.apiKey;
581-
if (config.anthropic.baseUrl) {
582-
modelConfig.base_url = config.anthropic.baseUrl;
583-
}
584-
}
585-
} else if (modelId.startsWith("gemini/")) {
586-
if (config.google?.apiKey) {
587-
modelConfig.api_key = config.google.apiKey;
666+
if (modelId.startsWith("gpt")) {
667+
const openai = providerConfig.openai;
668+
if (openai?.apiKey) {
669+
modelConfig.api_key = openai.apiKey;
670+
if (openai.baseUrl) {
671+
modelConfig.base_url = openai.baseUrl;
588672
}
589-
} else if (modelId.startsWith("groq/")) {
590-
if (config.groq?.apiKey) {
591-
modelConfig.api_key = config.groq.apiKey;
592-
}
593-
} else if (modelId.startsWith("deepseek/")) {
594-
if (config.deepseek?.apiKey) {
595-
modelConfig.api_key = config.deepseek.apiKey;
673+
}
674+
} else if (modelId.startsWith("claude")) {
675+
const anthropic = providerConfig.anthropic;
676+
if (anthropic?.apiKey) {
677+
modelConfig.api_key = anthropic.apiKey;
678+
if (anthropic.baseUrl) {
679+
modelConfig.base_url = anthropic.baseUrl;
596680
}
597681
}
598-
599-
safeUpdateOption("llm_config", modelConfig);
600-
} catch (err) {
601-
console.error("Failed to parse API keys:", err);
682+
} else if (modelId.startsWith("gemini/")) {
683+
const google = providerConfig.google;
684+
if (google?.apiKey) {
685+
modelConfig.api_key = google.apiKey;
686+
}
687+
} else if (modelId.startsWith("groq/")) {
688+
const groq = providerConfig.groq;
689+
if (groq?.apiKey) {
690+
modelConfig.api_key = groq.apiKey;
691+
}
692+
} else if (modelId.startsWith("deepseek/")) {
693+
const deepseek = providerConfig.deepseek;
694+
if (deepseek?.apiKey) {
695+
modelConfig.api_key = deepseek.apiKey;
696+
}
602697
}
698+
699+
safeUpdateOption("llm_config", modelConfig);
603700
}
604701
};
605702

@@ -626,12 +723,13 @@ const ChatSection: React.FC<ChatSectionProps> = ({
626723
// Load custom models, fetch configured providers, and combine with server models
627724
useEffect(() => {
628725
const loadModelsAndConfig = async () => {
629-
const allModels: Array<{
630-
id: string;
631-
name: string;
632-
provider: string;
633-
description?: string;
634-
}> = [...serverModels];
726+
const allModels: AvailableModel[] = serverModels.map(model => ({
727+
id: model.id,
728+
name: model.name,
729+
provider: model.provider,
730+
description: model.description,
731+
config: (model as AvailableModel).config,
732+
}));
635733

636734
try {
637735
// Load custom models from backend if authenticated
@@ -641,12 +739,15 @@ const ChatSection: React.FC<ChatSectionProps> = ({
641739
});
642740
if (resp.ok) {
643741
const customModelsList = await resp.json();
644-
const customTransformed = customModelsList.map((m: { id: string; name: string; provider: string }) => ({
645-
id: `custom_${m.id}`,
646-
name: m.name,
647-
provider: m.provider,
648-
description: `Custom ${m.provider} model`,
649-
}));
742+
const customTransformed = customModelsList.map(
743+
(m: { id: string; name: string; provider: string; config?: Record<string, unknown> }) => ({
744+
id: `custom_${m.id}`,
745+
name: m.name,
746+
provider: m.provider,
747+
description: `Custom ${m.provider} model`,
748+
config: m.config,
749+
})
750+
);
650751
allModels.push(...customTransformed);
651752
}
652753
} else {
@@ -655,12 +756,15 @@ const ChatSection: React.FC<ChatSectionProps> = ({
655756
if (savedModels) {
656757
try {
657758
const parsed = JSON.parse(savedModels);
658-
const customTransformed = parsed.map((m: { id: string; name: string; provider: string }) => ({
659-
id: `custom_${m.id}`,
660-
name: m.name,
661-
provider: m.provider,
662-
description: `Custom ${m.provider} model`,
663-
}));
759+
const customTransformed = parsed.map(
760+
(m: { id: string; name: string; provider: string; config?: Record<string, unknown> }) => ({
761+
id: `custom_${m.id}`,
762+
name: m.name,
763+
provider: m.provider,
764+
description: `Custom ${m.provider} model`,
765+
config: m.config,
766+
})
767+
);
664768
allModels.push(...customTransformed);
665769
} catch (err) {
666770
console.error("Failed to parse custom models:", err);

ee/ui-component/components/settings/SettingsSection.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ export function SettingsSection({ authToken }: SettingsSectionProps) {
183183
const uiHost = "localhost";
184184
const lemonadeUiUrl = `http://${uiHost}:${port}/api/v1`;
185185
const lemonadeBackendUrl = `http://${backendHost}:${port}/api/v1`;
186+
const directApiBase = `http://${LEMONADE_HOST_OPTIONS.direct.host}:${port}/api/v1`;
187+
const dockerApiBase = `http://${LEMONADE_HOST_OPTIONS.docker.host}:${port}/api/v1`;
186188
const healthUrl = `${lemonadeUiUrl}/health`;
187189

188190
setTestingLemonade(true);
@@ -275,6 +277,17 @@ export function SettingsSection({ authToken }: SettingsSectionProps) {
275277
model: `openai/${rawName}`,
276278
api_base: lemonadeBackendUrl,
277279
vision: rawName.toLowerCase().includes("vision") || rawName.toLowerCase().includes("vl"),
280+
lemonade_metadata: {
281+
host_mode: lemonadeHostMode,
282+
port,
283+
backend_host: backendHost,
284+
ui_api_base: lemonadeUiUrl,
285+
api_bases: {
286+
direct: directApiBase,
287+
docker: dockerApiBase,
288+
selected: lemonadeBackendUrl,
289+
},
290+
},
278291
},
279292
};
280293

ee/ui-component/hooks/useModels.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ interface Model {
55
name: string;
66
provider: string;
77
description?: string;
8+
config?: Record<string, unknown>;
9+
model?: string;
10+
modelKey?: string;
811
}
912

1013
interface ModelAPIResponse {
@@ -64,14 +67,22 @@ export function useModels(apiBaseUrl: string, authToken: string | null) {
6467
// Handle different response formats
6568
if (data.models) {
6669
// Direct models format
67-
transformedModels = data.models;
70+
transformedModels = data.models.map(model => ({
71+
...model,
72+
config: model.config,
73+
model: model.model,
74+
modelKey: model.modelKey,
75+
}));
6876
} else if (data.chat_models) {
6977
// Transform chat_models format
7078
transformedModels = data.chat_models.map(model => ({
7179
id: model.config.model_name || model.model,
7280
name: model.id.replace(/_/g, " ").replace(/\b\w/g, (l: string) => l.toUpperCase()),
7381
provider: model.provider,
7482
description: `Model: ${model.model}`,
83+
config: model.config,
84+
model: model.model,
85+
modelKey: model.id,
7586
}));
7687
}
7788

0 commit comments

Comments
 (0)