Skip to content

Commit a0bfda6

Browse files
authored
feat(setup): support custom OpenAI-compatible base URL provider (#76)
1 parent d57f7c9 commit a0bfda6

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

src/server.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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-Za-z0-9_-]+$/.test(providerId)) {
646+
extra += `\n[custom provider] skipped: invalid provider id (use letters/numbers/_/-)`;
647+
} else if (!/^https?:\/\//.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-Za-z_][A-Za-z0-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

src/setup-app.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,13 @@
100100
telegramToken: document.getElementById('telegramToken').value,
101101
discordToken: document.getElementById('discordToken').value,
102102
slackBotToken: document.getElementById('slackBotToken').value,
103-
slackAppToken: document.getElementById('slackAppToken').value
103+
slackAppToken: document.getElementById('slackAppToken').value,
104+
105+
customProviderId: document.getElementById('customProviderId').value,
106+
customProviderBaseUrl: document.getElementById('customProviderBaseUrl').value,
107+
customProviderApi: document.getElementById('customProviderApi').value,
108+
customProviderApiKeyEnv: document.getElementById('customProviderApiKeyEnv').value,
109+
customProviderModelId: document.getElementById('customProviderModelId').value
104110
};
105111

106112
logEl.textContent = 'Running...\n';

0 commit comments

Comments
 (0)