From c19cedaeded2dde309d375e10c773d60cea789b7 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 24 May 2026 09:34:24 -0400 Subject: [PATCH] Link unconfigured providers to prefilled secret form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/fabro-web/app/routes/settings-models.tsx | 9 ++++++ .../app/routes/settings-secrets-new.tsx | 5 +-- docs/public/api-reference/fabro-api.yaml | 8 +++++ lib/crates/fabro-model/src/catalog.rs | 24 ++++++++------ lib/crates/fabro-model/src/provider.rs | 31 +++++++++++++------ .../fabro-api-client/src/models/provider.ts | 4 +++ 6 files changed, 60 insertions(+), 21 deletions(-) diff --git a/apps/fabro-web/app/routes/settings-models.tsx b/apps/fabro-web/app/routes/settings-models.tsx index 1bde2da0e..091283ac8 100644 --- a/apps/fabro-web/app/routes/settings-models.tsx +++ b/apps/fabro-web/app/routes/settings-models.tsx @@ -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 → ) : null} + {!provider.configured && provider.expected_secret_name ? ( + + Add secret → + + ) : null} ); } diff --git a/apps/fabro-web/app/routes/settings-secrets-new.tsx b/apps/fabro-web/app/routes/settings-secrets-new.tsx index 8040f4e69..111d43e3a 100644 --- a/apps/fabro-web/app/routes/settings-secrets-new.tsx +++ b/apps/fabro-web/app/routes/settings-secrets-new.tsx @@ -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( 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); diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 91f812041..58a1e02e9 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -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. diff --git a/lib/crates/fabro-model/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs index d0fa36cb9..b1c931651 100644 --- a/lib/crates/fabro-model/src/catalog.rs +++ b/lib/crates/fabro-model/src/catalog.rs @@ -482,6 +482,20 @@ pub struct CatalogProvider { pub aliases: Vec, } +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, @@ -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] diff --git a/lib/crates/fabro-model/src/provider.rs b/lib/crates/fabro-model/src/provider.rs index 03b0caa8c..5d3e227be 100644 --- a/lib/crates/fabro-model/src/provider.rs +++ b/lib/crates/fabro-model/src/provider.rs @@ -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, + pub base_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub api_key_url: Option, - pub priority: i32, + pub api_key_url: Option, + pub priority: i32, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub aliases: Vec, + pub aliases: Vec, /// 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, + pub default_model: Option, /// 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, } 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"), + ); } } diff --git a/lib/packages/fabro-api-client/src/models/provider.ts b/lib/packages/fabro-api-client/src/models/provider.ts index 6d18c4473..901f1ca37 100644 --- a/lib/packages/fabro-api-client/src/models/provider.ts +++ b/lib/packages/fabro-api-client/src/models/provider.ts @@ -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 = {