fix(web): clarify unreachable model status and wording

- Show unreachable status in model cards instead of API key preview when service is down

- Keep API key placeholder preview in model settings whenever an API key is already saved

- Rename model status wording from configured to available across backend, frontend, and i18n

- Update backend model status tests to match renamed status semantics
This commit is contained in:
lc6464 2026-03-31 09:46:17 +08:00
parent 134779b266
commit 26612790fe
No known key found for this signature in database
GPG key ID: 53C61B42FEC71D6D
7 changed files with 21 additions and 20 deletions

View file

@ -16,7 +16,7 @@ import (
const modelProbeTimeout = 800 * time.Millisecond const modelProbeTimeout = 800 * time.Millisecond
const ( const (
modelStatusConfigured = "configured" modelStatusAvailable = "available"
modelStatusUnconfigured = "unconfigured" modelStatusUnconfigured = "unconfigured"
modelStatusUnreachable = "unreachable" modelStatusUnreachable = "unreachable"
) )
@ -60,11 +60,11 @@ func modelConfigurationStatus(m *config.ModelConfig) modelConfigurationSummary {
} }
if requiresRuntimeProbe(m) { if requiresRuntimeProbe(m) {
if probeLocalModelAvailability(m) { if probeLocalModelAvailability(m) {
return modelConfigurationSummary{Available: true, Status: modelStatusConfigured} return modelConfigurationSummary{Available: true, Status: modelStatusAvailable}
} }
return modelConfigurationSummary{Available: false, Status: modelStatusUnreachable} return modelConfigurationSummary{Available: false, Status: modelStatusUnreachable}
} }
return modelConfigurationSummary{Available: true, Status: modelStatusConfigured} return modelConfigurationSummary{Available: true, Status: modelStatusAvailable}
} }
func requiresRuntimeProbe(m *config.ModelConfig) bool { func requiresRuntimeProbe(m *config.ModelConfig) bool {

View file

@ -138,17 +138,17 @@ func TestHandleListModels_AvailabilityUsesRuntimeProbesForLocalModels(t *testing
if gotStatus["openai-oauth"] != modelStatusUnconfigured { if gotStatus["openai-oauth"] != modelStatusUnconfigured {
t.Fatalf("openai oauth model status = %q, want %q", gotStatus["openai-oauth"], modelStatusUnconfigured) t.Fatalf("openai oauth model status = %q, want %q", gotStatus["openai-oauth"], modelStatusUnconfigured)
} }
if gotStatus["vllm-local"] != modelStatusConfigured { if gotStatus["vllm-local"] != modelStatusAvailable {
t.Fatalf("vllm local model status = %q, want %q", gotStatus["vllm-local"], modelStatusConfigured) t.Fatalf("vllm local model status = %q, want %q", gotStatus["vllm-local"], modelStatusAvailable)
} }
if gotStatus["ollama-default"] != modelStatusConfigured { if gotStatus["ollama-default"] != modelStatusAvailable {
t.Fatalf("ollama default model status = %q, want %q", gotStatus["ollama-default"], modelStatusConfigured) t.Fatalf("ollama default model status = %q, want %q", gotStatus["ollama-default"], modelStatusAvailable)
} }
if gotStatus["vllm-remote"] != modelStatusConfigured { if gotStatus["vllm-remote"] != modelStatusAvailable {
t.Fatalf("remote vllm model status = %q, want %q", gotStatus["vllm-remote"], modelStatusConfigured) t.Fatalf("remote vllm model status = %q, want %q", gotStatus["vllm-remote"], modelStatusAvailable)
} }
if gotStatus["copilot-gpt-5.4"] != modelStatusConfigured { if gotStatus["copilot-gpt-5.4"] != modelStatusAvailable {
t.Fatalf("copilot model status = %q, want %q", gotStatus["copilot-gpt-5.4"], modelStatusConfigured) t.Fatalf("copilot model status = %q, want %q", gotStatus["copilot-gpt-5.4"], modelStatusAvailable)
} }
if len(openAIProbes) != 1 || openAIProbes[0] != "http://127.0.0.1:8000/v1|custom-model|" { if len(openAIProbes) != 1 || openAIProbes[0] != "http://127.0.0.1:8000/v1|custom-model|" {
t.Fatalf("openAI probes = %#v, want only local vllm probe", openAIProbes) t.Fatalf("openAI probes = %#v, want only local vllm probe", openAIProbes)

View file

@ -21,7 +21,7 @@ export interface ModelInfo {
extra_body?: Record<string, unknown> extra_body?: Record<string, unknown>
// Meta // Meta
available: boolean available: boolean
status?: "configured" | "unconfigured" | "unreachable" status?: "available" | "unconfigured" | "unreachable"
is_default: boolean is_default: boolean
is_virtual: boolean is_virtual: boolean
} }

View file

@ -133,9 +133,10 @@ export function EditModelSheet({
} }
const isOAuth = model?.auth_method === "oauth" const isOAuth = model?.auth_method === "oauth"
const apiKeyPlaceholder = model?.available const hasSavedAPIKey = Boolean(model?.api_key)
const apiKeyPlaceholder = hasSavedAPIKey
? maskedSecretPlaceholder( ? maskedSecretPlaceholder(
model.api_key, model?.api_key ?? "",
t("models.field.apiKeyPlaceholderSet"), t("models.field.apiKeyPlaceholderSet"),
) )
: t("models.field.apiKeyPlaceholder") : t("models.field.apiKeyPlaceholder")
@ -161,7 +162,7 @@ export function EditModelSheet({
<Field <Field
label={t("models.field.apiKey")} label={t("models.field.apiKey")}
hint={ hint={
model?.available ? t("models.edit.apiKeyHint") : undefined hasSavedAPIKey ? t("models.edit.apiKeyHint") : undefined
} }
> >
<KeyInput <KeyInput

View file

@ -28,7 +28,7 @@ export function ModelCard({
}: ModelCardProps) { }: ModelCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
const isOAuth = model.auth_method === "oauth" const isOAuth = model.auth_method === "oauth"
const status = model.status ?? (model.available ? "configured" : "unconfigured") const status = model.status ?? (model.available ? "available" : "unconfigured")
const statusLabel = t(`models.status.${status}`) const statusLabel = t(`models.status.${status}`)
const canSetDefault = const canSetDefault =
model.available && !model.is_default && !model.is_virtual model.available && !model.is_default && !model.is_virtual
@ -49,7 +49,7 @@ export function ModelCard({
"mt-0.5 h-2 w-2 shrink-0 rounded-full", "mt-0.5 h-2 w-2 shrink-0 rounded-full",
model.is_default model.is_default
? "bg-green-400 shadow-[0_0_0_2px_rgba(74,222,128,0.35)]" ? "bg-green-400 shadow-[0_0_0_2px_rgba(74,222,128,0.35)]"
: status === "configured" : status === "available"
? "bg-green-500" ? "bg-green-500"
: status === "unreachable" : status === "unreachable"
? "bg-amber-500" ? "bg-amber-500"
@ -127,7 +127,7 @@ export function ModelCard({
<span className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px] font-medium"> <span className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px] font-medium">
OAuth OAuth
</span> </span>
) : model.api_key ? ( ) : status === "available" && model.api_key ? (
<span className="text-muted-foreground/70 flex items-center gap-1 font-mono text-[11px]"> <span className="text-muted-foreground/70 flex items-center gap-1 font-mono text-[11px]">
<IconKey className="size-3" /> <IconKey className="size-3" />
{model.api_key} {model.api_key}

View file

@ -170,7 +170,7 @@
"noDefaultHintPrefix": "No default model set yet. Click", "noDefaultHintPrefix": "No default model set yet. Click",
"noDefaultHintSuffix": "to set one.", "noDefaultHintSuffix": "to set one.",
"status": { "status": {
"configured": "Configured", "available": "Available",
"unconfigured": "Not configured", "unconfigured": "Not configured",
"unreachable": "Service unreachable" "unreachable": "Service unreachable"
}, },

View file

@ -170,7 +170,7 @@
"noDefaultHintPrefix": "尚未设置默认模型,点击", "noDefaultHintPrefix": "尚未设置默认模型,点击",
"noDefaultHintSuffix": "设为默认。", "noDefaultHintSuffix": "设为默认。",
"status": { "status": {
"configured": "已配置", "available": "可用",
"unconfigured": "未配置", "unconfigured": "未配置",
"unreachable": "服务不可达" "unreachable": "服务不可达"
}, },