@@ -80,13 +80,83 @@ const ApiOptions = ({
8080
8181 const [ openAiModels , setOpenAiModels ] = useState < Record < string , ModelInfo > | null > ( null )
8282
83+ const [ customHeaders , setCustomHeaders ] = useState < [ string , string ] [ ] > ( ( ) => {
84+ const headers = apiConfiguration ?. openAiHeaders || { }
85+ return Object . entries ( headers )
86+ } )
87+
8388 const [ anthropicBaseUrlSelected , setAnthropicBaseUrlSelected ] = useState ( ! ! apiConfiguration ?. anthropicBaseUrl )
8489 const [ azureApiVersionSelected , setAzureApiVersionSelected ] = useState ( ! ! apiConfiguration ?. azureApiVersion )
8590 const [ openRouterBaseUrlSelected , setOpenRouterBaseUrlSelected ] = useState ( ! ! apiConfiguration ?. openRouterBaseUrl )
8691 const [ openAiLegacyFormatSelected , setOpenAiLegacyFormatSelected ] = useState ( ! ! apiConfiguration ?. openAiLegacyFormat )
8792 const [ googleGeminiBaseUrlSelected , setGoogleGeminiBaseUrlSelected ] = useState (
8893 ! ! apiConfiguration ?. googleGeminiBaseUrl ,
8994 )
95+
96+ const handleAddCustomHeader = useCallback ( ( ) => {
97+ // Only update the local state to show the new row in the UI
98+ setCustomHeaders ( ( prev ) => [ ...prev , [ "" , "" ] ] )
99+ // Do not update the main configuration yet, wait for user input
100+ } , [ ] )
101+
102+ const handleUpdateHeaderKey = useCallback ( ( index : number , newKey : string ) => {
103+ setCustomHeaders ( ( prev ) => {
104+ const updated = [ ...prev ]
105+ if ( updated [ index ] ) {
106+ updated [ index ] = [ newKey , updated [ index ] [ 1 ] ]
107+ }
108+ return updated
109+ } )
110+ } , [ ] )
111+
112+ const handleUpdateHeaderValue = useCallback ( ( index : number , newValue : string ) => {
113+ setCustomHeaders ( ( prev ) => {
114+ const updated = [ ...prev ]
115+ if ( updated [ index ] ) {
116+ updated [ index ] = [ updated [ index ] [ 0 ] , newValue ]
117+ }
118+ return updated
119+ } )
120+ } , [ ] )
121+
122+ const handleRemoveCustomHeader = useCallback ( ( index : number ) => {
123+ setCustomHeaders ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) )
124+ } , [ ] )
125+
126+ // Helper to convert array of tuples to object (filtering out empty keys)
127+ const convertHeadersToObject = ( headers : [ string , string ] [ ] ) : Record < string , string > => {
128+ const result : Record < string , string > = { }
129+
130+ // Process each header tuple
131+ for ( const [ key , value ] of headers ) {
132+ const trimmedKey = key . trim ( )
133+
134+ // Skip empty keys
135+ if ( ! trimmedKey ) continue
136+
137+ // For duplicates, the last one in the array wins
138+ // This matches how HTTP headers work in general
139+ result [ trimmedKey ] = value . trim ( )
140+ }
141+
142+ return result
143+ }
144+
145+ // Debounced effect to update the main configuration when local customHeaders state stabilizes
146+ useDebounce (
147+ ( ) => {
148+ const currentConfigHeaders = apiConfiguration ?. openAiHeaders || { }
149+ const newHeadersObject = convertHeadersToObject ( customHeaders )
150+
151+ // Only update if the processed object is different from the current config
152+ if ( JSON . stringify ( currentConfigHeaders ) !== JSON . stringify ( newHeadersObject ) ) {
153+ setApiConfigurationField ( "openAiHeaders" , newHeadersObject )
154+ }
155+ } ,
156+ 300 ,
157+ [ customHeaders , apiConfiguration ?. openAiHeaders , setApiConfigurationField ] ,
158+ )
159+
90160 const [ isDescriptionExpanded , setIsDescriptionExpanded ] = useState ( false )
91161 const noTransform = < T , > ( value : T ) => value
92162
@@ -123,13 +193,15 @@ const ApiOptions = ({
123193 useDebounce (
124194 ( ) => {
125195 if ( selectedProvider === "openai" ) {
196+ // Use our custom headers state to build the headers object
197+ const headerObject = convertHeadersToObject ( customHeaders )
126198 vscode . postMessage ( {
127199 type : "requestOpenAiModels" ,
128200 values : {
129201 baseUrl : apiConfiguration ?. openAiBaseUrl ,
130202 apiKey : apiConfiguration ?. openAiApiKey ,
131203 customHeaders : { } , // Reserved for any additional headers
132- openAiHeaders : apiConfiguration ?. openAiHeaders ,
204+ openAiHeaders : headerObject ,
133205 } ,
134206 } )
135207 } else if ( selectedProvider === "ollama" ) {
@@ -148,6 +220,7 @@ const ApiOptions = ({
148220 apiConfiguration ?. openAiApiKey ,
149221 apiConfiguration ?. ollamaBaseUrl ,
150222 apiConfiguration ?. lmStudioBaseUrl ,
223+ customHeaders ,
151224 ] ,
152225 )
153226
@@ -842,79 +915,33 @@ const ApiOptions = ({
842915 < VSCodeButton
843916 appearance = "icon"
844917 title = { t ( "settings:common.add" ) }
845- onClick = { ( ) => {
846- const currentHeaders = { ...( apiConfiguration ?. openAiHeaders || { } ) }
847- // Use an empty string as key - user will fill it in
848- // The key will be properly set when the user types in the field
849- currentHeaders [ "" ] = ""
850- handleInputChange ( "openAiHeaders" ) ( {
851- target : {
852- value : currentHeaders ,
853- } ,
854- } )
855- } } >
918+ onClick = { handleAddCustomHeader } >
856919 < span className = "codicon codicon-add" > </ span >
857920 </ VSCodeButton >
858921 </ div >
859- { Object . keys ( apiConfiguration ?. openAiHeaders || { } ) . length === 0 ? (
922+ { ! customHeaders . length ? (
860923 < div className = "text-sm text-vscode-descriptionForeground" >
861924 { t ( "settings:providers.noCustomHeaders" ) }
862925 </ div >
863926 ) : (
864- Object . entries ( apiConfiguration ?. openAiHeaders || { } ) . map ( ( [ key , value ] , index ) => (
927+ customHeaders . map ( ( [ key , value ] , index ) => (
865928 < div key = { index } className = "flex items-center mb-2" >
866929 < VSCodeTextField
867930 value = { key }
868931 className = "flex-1 mr-2"
869932 placeholder = { t ( "settings:providers.headerName" ) }
870- onInput = { ( e : any ) => {
871- const currentHeaders = apiConfiguration ?. openAiHeaders ?? { }
872- const newValue = e . target . value
873-
874- if ( newValue && newValue !== key ) {
875- const { [ key ] : _ , ...rest } = currentHeaders
876- handleInputChange ( "openAiHeaders" ) ( {
877- target : {
878- value : {
879- ...rest ,
880- [ newValue ] : value ,
881- } ,
882- } ,
883- } )
884- }
885- } }
933+ onInput = { ( e : any ) => handleUpdateHeaderKey ( index , e . target . value ) }
886934 />
887935 < VSCodeTextField
888936 value = { value }
889937 className = "flex-1 mr-2"
890938 placeholder = { t ( "settings:providers.headerValue" ) }
891- onInput = { ( e : any ) => {
892- handleInputChange ( "openAiHeaders" ) ( {
893- target : {
894- value : {
895- ...( apiConfiguration ?. openAiHeaders ?? { } ) ,
896- [ key ] : e . target . value ,
897- } ,
898- } ,
899- } )
900- } }
939+ onInput = { ( e : any ) => handleUpdateHeaderValue ( index , e . target . value ) }
901940 />
902941 < VSCodeButton
903942 appearance = "icon"
904943 title = { t ( "settings:common.remove" ) }
905- onClick = { ( ) => {
906- const { [ key ] : _ , ...rest } = apiConfiguration ?. openAiHeaders ?? { }
907-
908- // Ensure we always maintain an empty object even when removing the last header
909- // This prevents the field from becoming undefined or null
910- const newHeaders = Object . keys ( rest ) . length === 0 ? { } : rest
911-
912- handleInputChange ( "openAiHeaders" ) ( {
913- target : {
914- value : newHeaders ,
915- } ,
916- } )
917- } } >
944+ onClick = { ( ) => handleRemoveCustomHeader ( index ) } >
918945 < span className = "codicon codicon-trash" > </ span >
919946 </ VSCodeButton >
920947 </ div >
0 commit comments