@@ -47,6 +47,49 @@ interface ChatHistoryAPIItem {
47
47
} ;
48
48
}
49
49
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
+
50
93
/**
51
94
* ChatSection component using Vercel-style UI
52
95
*/
@@ -172,15 +215,7 @@ const ChatSection: React.FC<ChatSectionProps> = ({
172
215
> ( [ ] ) ;
173
216
174
217
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 [ ] > ( [ ] ) ;
184
219
185
220
// Provider configuration is derived on demand; no need to store separately
186
221
@@ -533,73 +568,135 @@ const ChatSection: React.FC<ChatSectionProps> = ({
533
568
const handleModelChange = ( modelId : string ) => {
534
569
setSelectedModel ( modelId ) ;
535
570
536
- // Handle default model - clear llm_config to use server default
537
571
if ( modelId === "default" ) {
538
572
safeUpdateOption ( "llm_config" , undefined ) ;
539
573
return ;
540
574
}
541
575
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
+
543
627
if ( modelId . startsWith ( "custom_" ) ) {
544
- const savedModels = localStorage . getItem ( "morphik_custom_models" ) ;
628
+ const savedModels = typeof window !== "undefined" ? localStorage . getItem ( "morphik_custom_models" ) : null ;
545
629
if ( savedModels ) {
546
630
try {
547
631
const customModels = JSON . parse ( savedModels ) ;
548
632
const customModel = customModels . find ( ( m : { id : string } ) => `custom_${ m . id } ` === modelId ) ;
549
633
550
634
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 ) ;
553
644
return ;
554
645
}
555
646
} catch ( err ) {
556
647
console . error ( "Failed to parse custom models:" , err ) ;
557
648
}
558
649
}
559
- }
560
650
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
+ }
566
661
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 } ;
569
665
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 ;
588
672
}
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 ;
596
680
}
597
681
}
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
+ }
602
697
}
698
+
699
+ safeUpdateOption ( "llm_config" , modelConfig ) ;
603
700
}
604
701
} ;
605
702
@@ -626,12 +723,13 @@ const ChatSection: React.FC<ChatSectionProps> = ({
626
723
// Load custom models, fetch configured providers, and combine with server models
627
724
useEffect ( ( ) => {
628
725
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
+ } ) ) ;
635
733
636
734
try {
637
735
// Load custom models from backend if authenticated
@@ -641,12 +739,15 @@ const ChatSection: React.FC<ChatSectionProps> = ({
641
739
} ) ;
642
740
if ( resp . ok ) {
643
741
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
+ ) ;
650
751
allModels . push ( ...customTransformed ) ;
651
752
}
652
753
} else {
@@ -655,12 +756,15 @@ const ChatSection: React.FC<ChatSectionProps> = ({
655
756
if ( savedModels ) {
656
757
try {
657
758
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
+ ) ;
664
768
allModels . push ( ...customTransformed ) ;
665
769
} catch ( err ) {
666
770
console . error ( "Failed to parse custom models:" , err ) ;
0 commit comments