feat(den): let users name their LLM providers explicitly (#1508)

Users were confused when adding or editing an LLM provider because the
display name was derived automatically from the catalog or pasted
config. This made it impossible to tell two keys for the same provider
apart (e.g. 'OpenAI personal' vs 'OpenAI prod').

- Add an explicit 'Name' field to the add + edit LLM provider forms in
  ee/apps/den-web with a 'Give this key a name' prompt, preload the
  current name when editing, and require a non-empty value before save.
- Require 'name' on the llm-provider write schema in ee/apps/den-api and
  persist it verbatim instead of falling back to provider.name from the
  models.dev catalog or the custom config JSON.

Verified end-to-end via Chrome DevTools MCP on the Den Docker stack;
screenshots captured under ee/apps/den-web/docs/screenshots/.

Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
Source Open
2026-04-20 17:27:00 -07:00
committed by GitHub
parent 609a57face
commit 307c8bcc6b
8 changed files with 31 additions and 3 deletions

View File

@@ -60,6 +60,7 @@ const customProviderSchema = z.object({
}).passthrough()
const llmProviderWriteSchema = z.object({
name: z.string().trim().min(1).max(255),
source: z.enum(["models_dev", "custom"]),
providerId: z.string().trim().min(1).max(255).optional(),
modelIds: z.array(z.string().trim().min(1).max(255)).min(1).optional(),
@@ -288,7 +289,7 @@ async function normalizeLlmProviderInput(input: z.infer<typeof llmProviderWriteS
return {
source: input.source,
providerId: provider.id,
name: provider.name,
name: input.name,
providerConfig: provider.config,
models: models.map((model) => ({
id: model.id,
@@ -320,7 +321,7 @@ async function normalizeLlmProviderInput(input: z.infer<typeof llmProviderWriteS
return {
source: input.source,
providerId: customProvider.data.id,
name: customProvider.data.name,
name: input.name,
providerConfig: providerConfig as JsonRecord,
models: models.map((model) => ({
id: model.id,

View File

@@ -81,6 +81,7 @@ export function LlmProviderEditorScreen({
useState<DenModelsDevProviderDetail | null>(null);
const [detailBusy, setDetailBusy] = useState(false);
const [detailError, setDetailError] = useState<string | null>(null);
const [providerName, setProviderName] = useState("");
const [selectedModelIds, setSelectedModelIds] = useState<string[]>([]);
const [modelQuery, setModelQuery] = useState("");
const [customConfigText, setCustomConfigText] = useState(
@@ -131,6 +132,7 @@ export function LlmProviderEditorScreen({
if (provider) {
setSource(provider.source);
setSelectedProviderId(provider.providerId);
setProviderName(provider.name);
setSelectedModelIds(provider.models.map((entry) => entry.id));
setSelectedMemberIds(
provider.access.members.map((entry) => entry.orgMembershipId),
@@ -149,6 +151,7 @@ export function LlmProviderEditorScreen({
setSource("models_dev");
setSelectedProviderId("");
setProviderName("");
setSelectedModelIds([]);
setSelectedMemberIds(
orgContext?.currentMember.id ? [orgContext.currentMember.id] : [],
@@ -261,6 +264,11 @@ export function LlmProviderEditorScreen({
return;
}
if (!providerName.trim()) {
setSaveError("Give this provider a name.");
return;
}
if (source === "models_dev") {
if (!selectedProviderId) {
setSaveError("Select a provider.");
@@ -281,6 +289,7 @@ export function LlmProviderEditorScreen({
setSaveError(null);
try {
const body: Record<string, unknown> = {
name: providerName.trim(),
source,
memberIds: [...new Set(selectedMemberIds)],
teamIds: [...new Set(selectedTeamIds)],
@@ -396,7 +405,7 @@ export function LlmProviderEditorScreen({
<div>
<h1 className="text-[34px] font-semibold tracking-[-0.07em] text-gray-950">
{provider
? provider.name
? (providerName.trim() || provider.name)
: "Add a new LLM provider"}
</h1>
<p className="mt-3 max-w-[720px] text-[16px] leading-8 text-gray-500">
@@ -435,6 +444,24 @@ export function LlmProviderEditorScreen({
</div>
) : null}
<section className="mb-8 rounded-[36px] border border-gray-200 bg-white p-8 shadow-[0_18px_48px_-34px_rgba(15,23,42,0.24)]">
<label className="grid gap-3">
<span className="text-[14px] font-medium text-gray-700">
Name
</span>
<DenInput
value={providerName}
onChange={(event) => setProviderName(event.target.value)}
placeholder="Give this key a name"
autoComplete="off"
/>
</label>
<p className="mt-3 text-[13px] text-gray-500">
Pick a clear label so teammates know which key or provider
setup they are using.
</p>
</section>
<section className="mb-8 rounded-[36px] border border-gray-200 bg-white p-8 shadow-[0_18px_48px_-34px_rgba(15,23,42,0.24)]">
<h2 className="mb-6 text-[24px] font-semibold tracking-[-0.05em] text-gray-950">
Provider type

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB