mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
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:
parent
95ca1b9619
commit
c19cedaede
6 changed files with 60 additions and 21 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue