@@ -407,6 +407,29 @@ app.get("/setup", requireSetupAuth, (_req, res) => {
407407 <input id="slackAppToken" type="password" placeholder="xapp-..." />
408408 </div>
409409
410+ <div class="card">
411+ <h2>2b) Advanced: Custom OpenAI-compatible provider (optional)</h2>
412+ <p class="muted">Use this to configure an OpenAI-compatible API that requires a custom base URL (e.g. Ollama, vLLM, LM Studio, hosted proxies). You usually set the API key as a Railway variable and reference it here.</p>
413+
414+ <label>Provider id (e.g. ollama, deepseek, myproxy)</label>
415+ <input id="customProviderId" placeholder="ollama" />
416+
417+ <label>Base URL (must include /v1, e.g. http://host:11434/v1)</label>
418+ <input id="customProviderBaseUrl" placeholder="http://127.0.0.1:11434/v1" />
419+
420+ <label>API (openai-completions or openai-responses)</label>
421+ <select id="customProviderApi">
422+ <option value="openai-completions">openai-completions</option>
423+ <option value="openai-responses">openai-responses</option>
424+ </select>
425+
426+ <label>API key env var name (optional, e.g. OLLAMA_API_KEY). Leave blank for no key.</label>
427+ <input id="customProviderApiKeyEnv" placeholder="OLLAMA_API_KEY" />
428+
429+ <label>Optional model id to register (e.g. llama3.1:8b)</label>
430+ <input id="customProviderModelId" placeholder="" />
431+ </div>
432+
410433 <div class="card">
411434 <h2>3) Run onboarding</h2>
412435 <button id="run">Run setup</button>
@@ -599,7 +622,7 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
599622
600623 const ok = onboard . code === 0 && isConfigured ( ) ;
601624
602- // Optional channel setup (only after successful onboarding, and only if the installed CLI supports it ).
625+ // Optional setup (only after successful onboarding).
603626 if ( ok ) {
604627 // Ensure gateway token is written into config so the browser UI can authenticate reliably.
605628 // (We also enforce loopback bind since the wrapper proxies externally.)
@@ -611,6 +634,40 @@ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
611634 await runCmd ( OPENCLAW_NODE , clawArgs ( [ "config" , "set" , "gateway.bind" , "loopback" ] ) ) ;
612635 await runCmd ( OPENCLAW_NODE , clawArgs ( [ "config" , "set" , "gateway.port" , String ( INTERNAL_GATEWAY_PORT ) ] ) ) ;
613636
637+ // Optional: configure a custom OpenAI-compatible provider (base URL) for advanced users.
638+ if ( payload . customProviderId ?. trim ( ) && payload . customProviderBaseUrl ?. trim ( ) ) {
639+ const providerId = payload . customProviderId . trim ( ) ;
640+ const baseUrl = payload . customProviderBaseUrl . trim ( ) ;
641+ const api = ( payload . customProviderApi || "openai-completions" ) . trim ( ) ;
642+ const apiKeyEnv = ( payload . customProviderApiKeyEnv || "" ) . trim ( ) ;
643+ const modelId = ( payload . customProviderModelId || "" ) . trim ( ) ;
644+
645+ if ( ! / ^ [ A - Z a - z 0 - 9 _ - ] + $ / . test ( providerId ) ) {
646+ extra += `\n[custom provider] skipped: invalid provider id (use letters/numbers/_/-)` ;
647+ } else if ( ! / ^ h t t p s ? : \/ \/ / . test ( baseUrl ) ) {
648+ extra += `\n[custom provider] skipped: baseUrl must start with http(s)://` ;
649+ } else if ( api !== "openai-completions" && api !== "openai-responses" ) {
650+ extra += `\n[custom provider] skipped: api must be openai-completions or openai-responses` ;
651+ } else if ( apiKeyEnv && ! / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * $ / . test ( apiKeyEnv ) ) {
652+ extra += `\n[custom provider] skipped: invalid api key env var name` ;
653+ } else {
654+ const providerCfg = {
655+ baseUrl,
656+ api,
657+ apiKey : apiKeyEnv ? "${" + apiKeyEnv + "}" : undefined ,
658+ models : modelId ? [ { id : modelId , name : modelId } ] : undefined ,
659+ } ;
660+
661+ // Ensure we merge in this provider rather than replacing other providers.
662+ await runCmd ( OPENCLAW_NODE , clawArgs ( [ "config" , "set" , "models.mode" , "merge" ] ) ) ;
663+ const set = await runCmd (
664+ OPENCLAW_NODE ,
665+ clawArgs ( [ "config" , "set" , "--json" , `models.providers.${ providerId } ` , JSON . stringify ( providerCfg ) ] ) ,
666+ ) ;
667+ extra += `\n[custom provider] exit=${ set . code } (output ${ set . output . length } chars)\n${ set . output || "(no output)" } ` ;
668+ }
669+ }
670+
614671 const channelsHelp = await runCmd ( OPENCLAW_NODE , clawArgs ( [ "channels" , "add" , "--help" ] ) ) ;
615672 const helpText = channelsHelp . output || "" ;
616673
0 commit comments