feat: add auto-fetch models from provider's /v1/models endpoint

Add ability to fetch available models from third-party aggregation
providers (SiliconFlow, OpenRouter, etc.) via OpenAI-compatible
GET /v1/models endpoint. Users can click "Fetch Models" button in
the provider form, then select models from a dropdown on each
model input field.

- Backend: new model_fetch service + Tauri command (Rust)
- Frontend: ModelInputWithFetch shared component
- Integrated into all 5 app forms (Claude/Codex/Gemini/OpenCode/OpenClaw)
- i18n support for zh/en/ja
This commit is contained in:
Jason
2026-04-03 23:10:17 +08:00
parent de49f6fbbe
commit 5017002938
16 changed files with 825 additions and 70 deletions
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
@@ -24,15 +24,16 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
import { ChevronDown, ChevronRight, Download, Loader2 } from "lucide-react";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField } from "./shared";
import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared";
import { CopilotAuthSection } from "./CopilotAuthSection";
import {
copilotGetModels,
copilotGetModelsForAccount,
} from "@/lib/api/copilot";
import type { CopilotModel } from "@/lib/api/copilot";
import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch";
import type {
ProviderCategory,
ClaudeApiFormat,
@@ -179,6 +180,34 @@ export function ClaudeFormFields({
const [copilotModels, setCopilotModels] = useState<CopilotModel[]>([]);
const [modelsLoading, setModelsLoading] = useState(false);
// 通用模型获取(非 Copilot 供应商)
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const handleFetchModels = useCallback(() => {
if (!baseUrl || !apiKey) {
toast.error(t("providerForm.fetchModelsFailed"));
return;
}
setIsFetchingModels(true);
fetchModelsForConfig(baseUrl, apiKey, isFullUrl)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
toast.info(t("providerForm.fetchModelsEmpty"));
} else {
toast.success(
t("providerForm.fetchModelsSuccess", { count: models.length }),
);
}
})
.catch((err) => {
console.warn("[ModelFetch] Failed:", err);
toast.error(t("providerForm.fetchModelsFailed"));
})
.finally(() => setIsFetchingModels(false));
}, [baseUrl, apiKey, isFullUrl, t]);
// 当 Copilot 预设且已认证时,加载可用模型
useEffect(() => {
// 如果不是 Copilot 预设或未认证,清空模型列表
@@ -298,14 +327,15 @@ export function ClaudeFormFields({
);
}
// 非 Copilot 供应商: 使用 ModelInputWithFetch(获取按钮在 section 标题旁)
return (
<Input
<ModelInputWithFetch
id={id}
type="text"
value={value}
onChange={(e) => onModelChange(field, e.target.value)}
onChange={(v) => onModelChange(field, v)}
placeholder={placeholder}
autoComplete="off"
fetchedModels={fetchedModels}
isLoading={isFetchingModels}
/>
);
};
@@ -502,7 +532,26 @@ export function ClaudeFormFields({
{/* 模型映射 */}
<div className="space-y-1 pt-2 border-t">
<FormLabel>{t("providerForm.modelMappingLabel")}</FormLabel>
<div className="flex items-center justify-between">
<FormLabel>{t("providerForm.modelMappingLabel")}</FormLabel>
{!isCopilotPreset && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleFetchModels}
disabled={isFetchingModels}
className="h-7 gap-1"
>
{isFetchingModels ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("providerForm.fetchModels")}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{t("providerForm.modelMappingHint")}
</p>
@@ -1,6 +1,11 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { Download, Loader2 } from "lucide-react";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField } from "./shared";
import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared";
import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch";
import type { ProviderCategory } from "@/types";
interface EndpointCandidate {
@@ -65,6 +70,33 @@ export function CodexFormFields({
}: CodexFormFieldsProps) {
const { t } = useTranslation();
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const handleFetchModels = useCallback(() => {
if (!codexBaseUrl || !codexApiKey) {
toast.error(t("providerForm.fetchModelsFailed"));
return;
}
setIsFetchingModels(true);
fetchModelsForConfig(codexBaseUrl, codexApiKey, isFullUrl)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
toast.info(t("providerForm.fetchModelsEmpty"));
} else {
toast.success(
t("providerForm.fetchModelsSuccess", { count: models.length }),
);
}
})
.catch((err) => {
console.warn("[ModelFetch] Failed:", err);
toast.error(t("providerForm.fetchModelsFailed"));
})
.finally(() => setIsFetchingModels(false));
}, [codexBaseUrl, codexApiKey, isFullUrl, t]);
return (
<>
{/* Codex API Key 输入框 */}
@@ -107,21 +139,38 @@ export function CodexFormFields({
{/* Codex Model Name 输入框 */}
{shouldShowModelField && onModelNameChange && (
<div className="space-y-2">
<label
htmlFor="codexModelName"
className="block text-sm font-medium text-foreground"
>
{t("codexConfig.modelName", { defaultValue: "模型名称" })}
</label>
<input
<div className="flex items-center justify-between">
<label
htmlFor="codexModelName"
className="block text-sm font-medium text-foreground"
>
{t("codexConfig.modelName", { defaultValue: "模型名称" })}
</label>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleFetchModels}
disabled={isFetchingModels}
className="h-7 gap-1"
>
{isFetchingModels ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("providerForm.fetchModels")}
</Button>
</div>
<ModelInputWithFetch
id="codexModelName"
type="text"
value={modelName}
onChange={(e) => onModelNameChange(e.target.value)}
onChange={(v) => onModelNameChange!(v)}
placeholder={t("codexConfig.modelNamePlaceholder", {
defaultValue: "例如: gpt-5.4",
})}
className="w-full px-3 py-2 border border-border-default bg-background text-foreground rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 transition-colors"
fetchedModels={fetchedModels}
isLoading={isFetchingModels}
/>
<p className="text-xs text-muted-foreground">
{modelName.trim()
@@ -1,9 +1,12 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Info } from "lucide-react";
import { Download, Info, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField } from "./shared";
import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared";
import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch";
import type { ProviderCategory } from "@/types";
interface EndpointCandidate {
@@ -66,6 +69,33 @@ export function GeminiFormFields({
}: GeminiFormFieldsProps) {
const { t } = useTranslation();
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const handleFetchModels = useCallback(() => {
if (!baseUrl || !apiKey) {
toast.error(t("providerForm.fetchModelsFailed"));
return;
}
setIsFetchingModels(true);
fetchModelsForConfig(baseUrl, apiKey)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
toast.info(t("providerForm.fetchModelsEmpty"));
} else {
toast.success(
t("providerForm.fetchModelsSuccess", { count: models.length }),
);
}
})
.catch((err) => {
console.warn("[ModelFetch] Failed:", err);
toast.error(t("providerForm.fetchModelsFailed"));
})
.finally(() => setIsFetchingModels(false));
}, [baseUrl, apiKey, t]);
// 检测是否为 Google 官方(使用 OAuth
const isGoogleOfficial =
partnerPromotionKey?.toLowerCase() === "google-official";
@@ -123,15 +153,34 @@ export function GeminiFormFields({
{/* Model 输入框 */}
{shouldShowModelField && (
<div>
<FormLabel htmlFor="gemini-model">
{t("provider.form.gemini.model", { defaultValue: "模型" })}
</FormLabel>
<Input
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel htmlFor="gemini-model">
{t("provider.form.gemini.model", { defaultValue: "模型" })}
</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleFetchModels}
disabled={isFetchingModels}
className="h-7 gap-1"
>
{isFetchingModels ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("providerForm.fetchModels")}
</Button>
</div>
<ModelInputWithFetch
id="gemini-model"
value={model}
onChange={(e) => onModelChange(e.target.value)}
onChange={onModelChange}
placeholder="gemini-3-pro-preview"
fetchedModels={fetchedModels}
isLoading={isFetchingModels}
/>
</div>
)}
@@ -16,9 +16,26 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react";
import { toast } from "sonner";
import {
Download,
Plus,
Trash2,
ChevronDown,
ChevronRight,
Loader2,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Checkbox } from "@/components/ui/checkbox";
import { ApiKeySection } from "./shared";
import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch";
import { openclawApiProtocols } from "@/config/openclawProviderPresets";
import type { ProviderCategory, OpenClawModel } from "@/types";
@@ -70,6 +87,8 @@ export function OpenClawFormFields({
const [expandedModels, setExpandedModels] = useState<Record<number, boolean>>(
{},
);
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
// Stable key tracking for models list
const modelKeysRef = useRef<string[]>([]);
@@ -107,6 +126,31 @@ export function OpenClawFormFields({
]);
};
// Fetch models from API
const handleFetchModels = useCallback(() => {
if (!baseUrl || !apiKey) {
toast.error(t("providerForm.fetchModelsFailed"));
return;
}
setIsFetchingModels(true);
fetchModelsForConfig(baseUrl, apiKey)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
toast.info(t("providerForm.fetchModelsEmpty"));
} else {
toast.success(
t("providerForm.fetchModelsSuccess", { count: models.length }),
);
}
})
.catch((err) => {
console.warn("[ModelFetch] Failed:", err);
toast.error(t("providerForm.fetchModelsFailed"));
})
.finally(() => setIsFetchingModels(false));
}, [baseUrl, apiKey, t]);
// Remove a model entry
const handleRemoveModel = (index: number) => {
modelKeysRef.current.splice(index, 1);
@@ -234,16 +278,33 @@ export function OpenClawFormFields({
<FormLabel>
{t("openclaw.models", { defaultValue: "模型列表" })}
</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddModel}
className="h-7 gap-1"
>
<Plus className="h-3.5 w-3.5" />
{t("openclaw.addModel", { defaultValue: "添加模型" })}
</Button>
<div className="flex gap-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleFetchModels}
disabled={isFetchingModels}
className="h-7 gap-1"
>
{isFetchingModels ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("providerForm.fetchModels")}
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddModel}
className="h-7 gap-1"
>
<Plus className="h-3.5 w-3.5" />
{t("openclaw.addModel", { defaultValue: "添加模型" })}
</Button>
</div>
</div>
{models.length === 0 ? (
@@ -283,15 +344,66 @@ export function OpenClawFormFields({
<label className="text-xs text-muted-foreground">
{t("openclaw.modelId", { defaultValue: "模型 ID" })}
</label>
<Input
value={model.id}
onChange={(e) =>
handleModelChange(index, "id", e.target.value)
}
placeholder={t("openclaw.modelIdPlaceholder", {
defaultValue: "claude-3-sonnet",
})}
/>
<div className="flex gap-1">
<Input
value={model.id}
onChange={(e) =>
handleModelChange(index, "id", e.target.value)
}
placeholder={t("openclaw.modelIdPlaceholder", {
defaultValue: "claude-3-sonnet",
})}
className="flex-1"
/>
{fetchedModels.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0"
>
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="max-h-64 overflow-y-auto z-[200]"
>
{Object.entries(
fetchedModels.reduce(
(acc, m) => {
const v = m.ownedBy || "Other";
if (!acc[v]) acc[v] = [];
acc[v].push(m);
return acc;
},
{} as Record<string, FetchedModel[]>,
),
)
.sort(([a], [b]) => a.localeCompare(b))
.map(([vendor, vModels], vi) => (
<div key={vendor}>
{vi > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel>
{vendor}
</DropdownMenuLabel>
{vModels.map((m) => (
<DropdownMenuItem
key={m.id}
onSelect={() =>
handleModelChange(index, "id", m.id)
}
>
{m.id}
</DropdownMenuItem>
))}
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
<div className="flex-1 space-y-1">
<label className="text-xs text-muted-foreground">
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
@@ -10,8 +10,25 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Plus, Trash2, ChevronRight } from "lucide-react";
import { toast } from "sonner";
import {
ChevronDown,
Download,
Plus,
Trash2,
ChevronRight,
Loader2,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ApiKeySection } from "./shared";
import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch";
import { opencodeNpmPackages } from "@/config/opencodeProviderPresets";
import { cn } from "@/lib/utils";
import {
@@ -136,6 +153,49 @@ function ModelOptionKeyInput({
);
}
/** Dropdown button to select from fetched models */
function ModelDropdown({
models,
onSelect,
}: {
models: FetchedModel[];
onSelect: (id: string) => void;
}) {
const grouped: Record<string, FetchedModel[]> = {};
for (const model of models) {
const vendor = model.ownedBy || "Other";
if (!grouped[vendor]) grouped[vendor] = [];
grouped[vendor].push(model);
}
const vendors = Object.keys(grouped).sort();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="shrink-0">
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="max-h-64 overflow-y-auto z-[200]"
>
{vendors.map((vendor, vi) => (
<div key={vendor}>
{vi > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel>{vendor}</DropdownMenuLabel>
{grouped[vendor].map((m) => (
<DropdownMenuItem key={m.id} onSelect={() => onSelect(m.id)}>
{m.id}
</DropdownMenuItem>
))}
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
interface OpenCodeFormFieldsProps {
// NPM Package
npm: string;
@@ -182,6 +242,33 @@ export function OpenCodeFormFields({
}: OpenCodeFormFieldsProps) {
const { t } = useTranslation();
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const handleFetchModels = useCallback(() => {
if (!baseUrl || !apiKey) {
toast.error(t("providerForm.fetchModelsFailed"));
return;
}
setIsFetchingModels(true);
fetchModelsForConfig(baseUrl, apiKey)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
toast.info(t("providerForm.fetchModelsEmpty"));
} else {
toast.success(
t("providerForm.fetchModelsSuccess", { count: models.length }),
);
}
})
.catch((err) => {
console.warn("[ModelFetch] Failed:", err);
toast.error(t("providerForm.fetchModelsFailed"));
})
.finally(() => setIsFetchingModels(false));
}, [baseUrl, apiKey, t]);
// Track which models have expanded options panel
const [expandedModels, setExpandedModels] = useState<Set<string>>(new Set());
@@ -552,16 +639,33 @@ export function OpenCodeFormFields({
<FormLabel>
{t("opencode.models", { defaultValue: "Models" })}
</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddModel}
className="h-7 gap-1"
>
<Plus className="h-3.5 w-3.5" />
{t("opencode.addModel", { defaultValue: "Add" })}
</Button>
<div className="flex gap-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleFetchModels}
disabled={isFetchingModels}
className="h-7 gap-1"
>
{isFetchingModels ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("providerForm.fetchModels")}
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddModel}
className="h-7 gap-1"
>
<Plus className="h-3.5 w-3.5" />
{t("opencode.addModel", { defaultValue: "Add" })}
</Button>
</div>
</div>
{Object.keys(models).length === 0 ? (
@@ -600,13 +704,21 @@ export function OpenCodeFormFields({
)}
/>
</Button>
<ModelIdInput
modelId={key}
onChange={(newId) => handleModelIdChange(key, newId)}
placeholder={t("opencode.modelId", {
defaultValue: "Model ID",
})}
/>
<div className="flex gap-1 flex-1">
<ModelIdInput
modelId={key}
onChange={(newId) => handleModelIdChange(key, newId)}
placeholder={t("opencode.modelId", {
defaultValue: "Model ID",
})}
/>
{fetchedModels.length > 0 && (
<ModelDropdown
models={fetchedModels}
onSelect={(id) => handleModelIdChange(key, id)}
/>
)}
</div>
<Input
value={model.name}
onChange={(e) => handleModelNameChange(key, e.target.value)}
@@ -0,0 +1,146 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDown, Download, Loader2 } from "lucide-react";
import type { FetchedModel } from "@/lib/api/model-fetch";
interface ModelInputWithFetchProps {
id: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
fetchedModels: FetchedModel[];
isLoading: boolean;
/** 传入时显示获取按钮;不传时只在有数据后显示下拉 */
onFetch?: () => void;
}
export function ModelInputWithFetch({
id,
value,
onChange,
placeholder,
fetchedModels,
isLoading,
onFetch,
}: ModelInputWithFetchProps) {
const { t } = useTranslation();
// 有模型数据: Input + DropdownMenu
if (fetchedModels.length > 0) {
const grouped: Record<string, FetchedModel[]> = {};
for (const model of fetchedModels) {
const vendor = model.ownedBy || "Other";
if (!grouped[vendor]) grouped[vendor] = [];
grouped[vendor].push(model);
}
const vendors = Object.keys(grouped).sort();
return (
<div className="flex gap-1">
<Input
id={id}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
autoComplete="off"
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="shrink-0">
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="max-h-64 overflow-y-auto z-[200]"
>
{vendors.map((vendor, vi) => (
<div key={vendor}>
{vi > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel>{vendor}</DropdownMenuLabel>
{grouped[vendor].map((model) => (
<DropdownMenuItem
key={model.id}
onSelect={() => onChange(model.id)}
>
{model.id}
</DropdownMenuItem>
))}
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
// 加载中: Input + Spinner
if (isLoading) {
return (
<div className="flex gap-1">
<Input
id={id}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
autoComplete="off"
className="flex-1"
/>
<Button variant="outline" size="icon" className="shrink-0" disabled>
<Loader2 className="h-4 w-4 animate-spin" />
</Button>
</div>
);
}
// 有 onFetch: Input + 获取按钮
if (onFetch) {
return (
<div className="flex gap-1">
<Input
id={id}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
autoComplete="off"
className="flex-1"
/>
<Button
variant="outline"
size="icon"
className="shrink-0"
type="button"
onClick={onFetch}
title={t("providerForm.fetchModels")}
>
<Download className="h-4 w-4" />
</Button>
</div>
);
}
// 无 onFetch: 纯 Input
return (
<Input
id={id}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
autoComplete="off"
/>
);
}
@@ -1,2 +1,3 @@
export { ApiKeySection } from "./ApiKeySection";
export { EndpointField } from "./EndpointField";
export { ModelInputWithFetch } from "./ModelInputWithFetch";