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 = {