Add custom API URL and model to AI settings
- Settings → AI: optional base URL and model name fields - Defaults to Anthropic/OpenAI public APIs when left blank - Custom URL enables Open WebUI, LM Studio, Ollama, and any OpenAI-compatible endpoint - Parse endpoint uses custom base URL and model if configured - Migration 0004: ai_base_url + ai_model columns on users - OpenAI provider label updated to "OpenAI-compatible" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b1c160f607
commit
d6118bac54
6 changed files with 124 additions and 29 deletions
|
|
@ -658,6 +658,8 @@ function AiSection() {
|
|||
const qc = useQueryClient();
|
||||
const [provider, setProvider] = useState("anthropic");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [model, setModel] = useState("");
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
|
|
@ -666,16 +668,23 @@ function AiSection() {
|
|||
queryFn: async () => {
|
||||
const d = await getAiSettings();
|
||||
if (d.provider) setProvider(d.provider);
|
||||
if (d.base_url) setBaseUrl(d.base_url);
|
||||
if (d.model) setModel(d.model);
|
||||
return d;
|
||||
},
|
||||
});
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: () => saveAiSettings(provider, apiKey),
|
||||
mutationFn: () => saveAiSettings({
|
||||
provider,
|
||||
api_key: apiKey,
|
||||
base_url: baseUrl,
|
||||
model,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["ai-settings"] });
|
||||
setApiKey("");
|
||||
setSuccess("API key saved");
|
||||
setSuccess("Settings saved");
|
||||
setTimeout(() => setSuccess(""), 3000);
|
||||
},
|
||||
});
|
||||
|
|
@ -684,11 +693,15 @@ function AiSection() {
|
|||
mutationFn: clearAiSettings,
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["ai-settings"] });
|
||||
setSuccess("API key removed");
|
||||
setBaseUrl(""); setModel("");
|
||||
setSuccess("AI settings removed");
|
||||
setTimeout(() => setSuccess(""), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const defaultModel = provider === "anthropic" ? "claude-haiku-4-5-20251001" : "gpt-4o-mini";
|
||||
const defaultUrl = provider === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com";
|
||||
|
||||
return (
|
||||
<div className={cardCls}>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -723,8 +736,11 @@ function AiSection() {
|
|||
className={inputCls}
|
||||
>
|
||||
<option value="anthropic">Anthropic (Claude)</option>
|
||||
<option value="openai">OpenAI (GPT-4o mini)</option>
|
||||
<option value="openai">OpenAI-compatible</option>
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
OpenAI-compatible works with Open WebUI, LM Studio, Ollama, and any OpenAI-spec endpoint.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -737,7 +753,7 @@ function AiSection() {
|
|||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
className={cn(inputCls, "pr-10")}
|
||||
placeholder={current?.has_api_key ? "••••••••••••••••" : provider === "anthropic" ? "sk-ant-..." : "sk-..."}
|
||||
placeholder={current?.has_api_key ? "••••••••••••••••" : provider === "anthropic" ? "sk-ant-..." : "sk-... (use any value if not required)"}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -747,10 +763,33 @@ function AiSection() {
|
|||
{showKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-1.5">Custom API URL <span className="text-muted-foreground font-normal">(optional)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={baseUrl}
|
||||
onChange={e => setBaseUrl(e.target.value)}
|
||||
className={inputCls}
|
||||
placeholder={defaultUrl}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{provider === "anthropic"
|
||||
? "Get your key at console.anthropic.com → API Keys"
|
||||
: "Get your key at platform.openai.com → API Keys"}
|
||||
Leave blank to use the default. For Open WebUI: <code className="bg-secondary px-1 rounded">http://your-server:3000</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-1.5">Model <span className="text-muted-foreground font-normal">(optional)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={model}
|
||||
onChange={e => setModel(e.target.value)}
|
||||
className={inputCls}
|
||||
placeholder={defaultModel}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Leave blank to use the default. For Open WebUI, enter the model name exactly as shown in its interface.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -761,7 +800,7 @@ function AiSection() {
|
|||
className="flex items-center gap-2 bg-primary text-primary-foreground px-4 py-2 rounded-lg text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{saveMutation.isPending && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{current?.has_api_key && !apiKey ? "Update provider" : "Save API key"}
|
||||
Save settings
|
||||
</button>
|
||||
|
||||
{current?.has_api_key && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue