Link unconfigured providers to prefilled secret form

On /settings/models, unconfigured providers now offer "Add secret →"
alongside "Get API key →", deep-linking to /settings/secrets/new with
the expected vault secret name prefilled. Driven by a new
`expected_secret_name` field on the Provider API, derived from the
first vault credential in the catalog so the suggestion stays in sync
with the catalog instead of being hardcoded on the frontend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-24 09:34:24 -04:00
parent 95ca1b9619
commit c19cedaede
No known key found for this signature in database
6 changed files with 60 additions and 21 deletions

View file

@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { Link } from "react-router";
import { ChevronDownIcon } from "@heroicons/react/16/solid";
import type { Provider } from "@qltysh/fabro-api-client";
import { useProviders } from "../lib/queries";
@ -153,6 +154,14 @@ function ProviderStatus({ provider }: { provider: Provider }) {
Get API key
</a>
) : null}
{!provider.configured && provider.expected_secret_name ? (
<Link
to={`/settings/secrets/new?name=${encodeURIComponent(provider.expected_secret_name)}`}
className="text-xs text-teal-500 hover:underline"
>
Add secret
</Link>
) : null}
</span>
);
}

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import { Link, useNavigate } from "react-router";
import { Link, useNavigate, useSearchParams } from "react-router";
import { useSWRConfig } from "swr";
import { ArrowLeftIcon } from "@heroicons/react/16/solid";
import { SecretType } from "@qltysh/fabro-api-client";
@ -46,12 +46,13 @@ export default function SettingsSecretsNew() {
function CreateSecretForm() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { mutate } = useSWRConfig();
const toast = useToast();
const [type, setType] = useState<typeof SecretType.TOKEN | typeof SecretType.FILE>(
SecretType.TOKEN,
);
const [name, setName] = useState("");
const [name, setName] = useState(() => searchParams.get("name") ?? "");
const [value, setValue] = useState("");
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);

View file

@ -6275,6 +6275,14 @@ components:
Whether credential material is present for this provider on the
server when this response was produced. Does NOT imply requests
will succeed.
expected_secret_name:
type: ["string", "null"]
description: |
Suggested vault secret name for configuring this provider,
derived from the first vault credential reference in the
provider catalog. Null when the provider has no vault
credential (e.g. no-auth or env-only providers). Used to
prefill the create-secret form.
ProviderId:
description: LLM provider identifier.

View file

@ -482,6 +482,20 @@ pub struct CatalogProvider {
pub aliases: Vec<String>,
}
impl CatalogProvider {
#[must_use]
pub fn vault_secret_name(&self) -> Option<&str> {
self.auth
.as_ref()?
.credentials
.iter()
.find_map(|credential_ref| match credential_ref {
CredentialRef::Vault(name) => Some(name.as_str()),
CredentialRef::Env(_) => None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogModelControls {
pub reasoning_effort: Vec<ReasoningEffort>,
@ -841,15 +855,7 @@ impl Catalog {
#[must_use]
pub fn provider_vault_secret_name(&self, id: &ProviderId) -> Option<&str> {
self.provider(id)?
.auth
.as_ref()?
.credentials
.iter()
.find_map(|credential_ref| match credential_ref {
CredentialRef::Vault(name) => Some(name.as_str()),
CredentialRef::Env(_) => None,
})
self.provider(id)?.vault_secret_name()
}
#[must_use]

View file

@ -11,27 +11,33 @@ use crate::ids::ProviderId;
/// `agent_profile`) so credential material never reaches the wire.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Provider {
pub id: ProviderId,
pub display_name: String,
pub adapter: AdapterKind,
pub id: ProviderId,
pub display_name: String,
pub adapter: AdapterKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_url: Option<String>,
pub priority: i32,
pub api_key_url: Option<String>,
pub priority: i32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
pub aliases: Vec<String>,
/// Number of catalog models for this provider. Stamped by the handler.
pub model_count: u32,
pub model_count: u32,
/// Catalog default model ID for this provider, if any. Stamped by the
/// handler.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model: Option<String>,
pub default_model: Option<String>,
/// True if the server has credential material configured for this provider
/// when the response is produced. Always `false` in static catalog data;
/// stamped by `GET /providers` per request.
#[serde(default)]
pub configured: bool,
pub configured: bool,
/// Suggested vault secret name for configuring this provider, derived
/// from the first vault credential in the catalog. `None` when the
/// provider has no vault credential (e.g. Ollama, env-only providers).
/// Used by the web UI to prefill the create-secret form.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_secret_name: Option<String>,
}
impl Provider {
@ -53,6 +59,7 @@ impl Provider {
model_count,
default_model,
configured,
expected_secret_name: provider.vault_secret_name().map(str::to_owned),
}
}
}
@ -80,5 +87,9 @@ mod tests {
assert_eq!(provider.model_count, 7);
assert_eq!(provider.default_model.as_deref(), Some("claude-opus-4-7"));
assert!(provider.configured);
assert_eq!(
provider.expected_secret_name.as_deref(),
Some("ANTHROPIC_API_KEY"),
);
}
}

View file

@ -58,6 +58,10 @@ export interface Provider {
* Whether credential material is present for this provider on the server when this response was produced. Does NOT imply requests will succeed.
*/
'configured': boolean;
/**
* Suggested vault secret name for configuring this provider, derived from the first vault credential reference in the provider catalog. Null when the provider has no vault credential (e.g. no-auth or env-only providers). Used to prefill the create-secret form.
*/
'expected_secret_name'?: string | null;
}
export const ProviderAdapterEnum = {