@@ -586,6 +586,10 @@ <h2 style="text-align:center">Configuration Saved!</h2>
586586 id : "azure" , name : "Azure OpenAI" , desc : "Enterprise Azure-hosted models" , tag : "Enterprise" , free : false ,
587587 baseUrl : "" , api : "openai-responses"
588588 } ,
589+ {
590+ id : "custom" , name : "Custom Provider" , desc : "Any OpenAI-compatible API endpoint" , tag : "Advanced" , free : false ,
591+ baseUrl : "" , api : "openai-completions"
592+ } ,
589593 ] ;
590594
591595 let currentStep = 0 ;
@@ -641,14 +645,21 @@ <h2 style="text-align:center">Configuration Saved!</h2>
641645 const providerIds = Object . keys ( providers ) ;
642646 if ( providerIds . length > 0 ) {
643647 const pid = providerIds [ 0 ] ;
644- selectProviderCard ( pid ) ;
648+ const knownProviderIds = PROVIDERS . map ( p => p . id ) ;
649+ const isKnown = knownProviderIds . includes ( pid ) ;
650+ const cardId = isKnown ? pid : "custom" ;
651+ selectProviderCard ( cardId ) ;
645652
646653 // Store loaded provider details for step 2 pre-fill
647- existingConfig . _loadedProvider = pid ;
654+ existingConfig . _loadedProvider = cardId ;
648655 existingConfig . _loadedApiKey = providers [ pid ] . apiKey || "" ;
649656 existingConfig . _loadedBaseUrl = providers [ pid ] . baseUrl || "" ;
650657 const models = providers [ pid ] . models || [ ] ;
651658 existingConfig . _loadedModelId = models . length > 0 ? models [ 0 ] . id : "" ;
659+ if ( ! isKnown ) {
660+ existingConfig . _loadedCustomName = pid ;
661+ existingConfig . _loadedCustomApi = providers [ pid ] . api || "openai-completions" ;
662+ }
652663 }
653664
654665 // Telegram
@@ -786,6 +797,64 @@ <h2>Configure ${p.name}</h2>
786797 </div>
787798 ` ;
788799 setupCustomModelToggle ( azureDeployment ) ;
800+ } else if ( p . id === "custom" ) {
801+ let customName = "" ;
802+ let customBaseUrl = "" ;
803+ let customApiKey = "" ;
804+ let customModelId = "" ;
805+ let customApi = "openai-completions" ;
806+ if ( existingConfig && existingConfig . _loadedProvider === "custom" ) {
807+ customBaseUrl = existingConfig . _loadedBaseUrl || "" ;
808+ customApiKey = existingConfig . _loadedApiKey || "" ;
809+ customModelId = existingConfig . _loadedModelId || "" ;
810+ const provCfg = existingConfig . models && existingConfig . models . providers && existingConfig . models . providers . custom ;
811+ if ( provCfg ) customApi = provCfg . api || "openai-completions" ;
812+ customName = existingConfig . _loadedCustomName || "" ;
813+ }
814+
815+ container . innerHTML = `
816+ <h2>Configure Custom Provider</h2>
817+ <div class="subtitle">Connect to any OpenAI-compatible API endpoint (NVIDIA, Moonshot/Kimi, vLLM, LiteLLM, etc.)</div>
818+ <div class="field">
819+ <label>Provider Name</label>
820+ <div class="hint">A short name for this provider (e.g. "nvidia", "kimi"). Used internally as an identifier.</div>
821+ <input type="text" id="custom-provider-name" placeholder="nvidia" value="${ customName } ">
822+ </div>
823+ <div class="field">
824+ <label>Base URL</label>
825+ <div class="hint">The API base URL (e.g. https://integrate.api.nvidia.com/v1)</div>
826+ <input type="text" id="custom-base-url" placeholder="https://integrate.api.nvidia.com/v1" value="${ customBaseUrl } ">
827+ </div>
828+ <div class="field">
829+ <label>API Key</label>
830+ <div class="hint">Your API key for this provider.</div>
831+ <input type="password" id="api-key" placeholder="Paste your API key here" value="${ customApiKey } ">
832+ </div>
833+ <div class="field">
834+ <label>Model ID</label>
835+ <div class="hint">The exact model identifier (e.g. moonshotai/kimi-k2.5, meta/llama-3.1-70b-instruct)</div>
836+ <input type="text" id="custom-model-id" placeholder="moonshotai/kimi-k2.5" value="${ customModelId } ">
837+ </div>
838+ <div class="field">
839+ <label>API Type</label>
840+ <div class="hint">Most custom providers use the OpenAI-compatible completions API.</div>
841+ <select id="custom-api-type">
842+ <option value="openai-completions" ${ customApi === 'openai-completions' ? 'selected' : '' } >OpenAI Completions (most common)</option>
843+ <option value="openai-responses" ${ customApi === 'openai-responses' ? 'selected' : '' } >OpenAI Responses</option>
844+ <option value="anthropic-messages" ${ customApi === 'anthropic-messages' ? 'selected' : '' } >Anthropic Messages</option>
845+ </select>
846+ </div>
847+ <div class="field">
848+ <label>Context Window <span class="optional-badge">optional</span></label>
849+ <div class="hint">Max input tokens (default: 128000)</div>
850+ <input type="number" id="custom-context-window" placeholder="128000" value="">
851+ </div>
852+ <div class="field">
853+ <label>Max Output Tokens <span class="optional-badge">optional</span></label>
854+ <div class="hint">Max tokens the model can generate (default: 16384)</div>
855+ <input type="number" id="custom-max-tokens" placeholder="16384" value="">
856+ </div>
857+ ` ;
789858 } else {
790859 // Cloud providers with model selection
791860 const keyLabels = {
@@ -992,6 +1061,29 @@ <h2>Configure ${p.name}</h2>
9921061 } ]
9931062 } ;
9941063 config . agents . defaults . model . primary = `azure/${ modelId } ` ;
1064+ } else if ( p . id === "custom" ) {
1065+ const providerName = ( document . getElementById ( "custom-provider-name" ) . value . trim ( ) || "custom" ) . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 _ - ] / g, "-" ) ;
1066+ const baseUrl = document . getElementById ( "custom-base-url" ) . value . trim ( ) ;
1067+ const apiKey = document . getElementById ( "api-key" ) . value . trim ( ) ;
1068+ const modelId = document . getElementById ( "custom-model-id" ) . value . trim ( ) || "default" ;
1069+ const apiType = document . getElementById ( "custom-api-type" ) . value ;
1070+ const contextWindow = parseInt ( document . getElementById ( "custom-context-window" ) . value ) || 128000 ;
1071+ const maxTokens = parseInt ( document . getElementById ( "custom-max-tokens" ) . value ) || 16384 ;
1072+
1073+ config . models . providers [ providerName ] = {
1074+ baseUrl : baseUrl ,
1075+ apiKey : apiKey ,
1076+ api : apiType ,
1077+ models : [ {
1078+ id : modelId ,
1079+ name : modelId ,
1080+ reasoning : false ,
1081+ input : [ "text" ] ,
1082+ contextWindow : contextWindow ,
1083+ maxTokens : maxTokens
1084+ } ]
1085+ } ;
1086+ config . agents . defaults . model . primary = `${ providerName } /${ modelId } ` ;
9951087 } else {
9961088 const apiKey = document . getElementById ( "api-key" ) . value . trim ( ) ;
9971089 const model = getSelectedModel ( ) ;
0 commit comments