fix(web,api): fix edit test connection false negative and gate fetch for unsupported providers
- handleTestInlineModel now accepts optional model_index to fall back to stored credentials when api_key is empty, fixing false negatives when testing edited models - Add supportsFetch to provider registry and FETCHABLE_PROVIDER_KEYS derived set - Gate Fetch Models button to only show for OpenAI-compatible and Ollama providers - Add backend guard in handleFetchModels to reject unsupported providers with clear error
This commit is contained in:
parent
249fa93e95
commit
9a0edf0ae3
6 changed files with 93 additions and 22 deletions
|
|
@ -17,6 +17,18 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
// fetchableProviders lists providers that support OpenAI-compatible /models listing.
|
||||
var fetchableProviders = map[string]bool{
|
||||
"openai": true, "deepseek": true, "openrouter": true,
|
||||
"qwen-portal": true, "qwen-intl": true, "moonshot": true,
|
||||
"volcengine": true, "zhipu": true, "groq": true,
|
||||
"mistral": true, "nvidia": true, "cerebras": true,
|
||||
"venice": true, "shengsuanyun": true, "vivgrid": true,
|
||||
"minimax": true, "longcat": true, "modelscope": true,
|
||||
"mimo": true, "avian": true, "zai": true, "novita": true,
|
||||
"litellm": true, "vllm": true, "lmstudio": true, "ollama": true,
|
||||
}
|
||||
|
||||
// registerModelRoutes binds model list management endpoints to the ServeMux.
|
||||
func (h *Handler) registerModelRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/models", h.handleListModels)
|
||||
|
|
@ -684,6 +696,7 @@ func (h *Handler) handleTestInlineModel(w http.ResponseWriter, r *http.Request)
|
|||
APIBase string `json:"api_base"`
|
||||
APIKey string `json:"api_key"`
|
||||
AuthMethod string `json:"auth_method"`
|
||||
ModelIndex *int `json:"model_index"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
|
|
@ -700,6 +713,21 @@ func (h *Handler) handleTestInlineModel(w http.ResponseWriter, r *http.Request)
|
|||
m.SetAPIKey(req.APIKey)
|
||||
}
|
||||
|
||||
// When api_key is empty and model_index is provided, fall back to stored credentials.
|
||||
// This lets the edit form test unsaved field changes while using the saved key.
|
||||
if req.APIKey == "" && req.ModelIndex != nil {
|
||||
cfg, err := config.LoadConfig(h.configPath)
|
||||
if err == nil && *req.ModelIndex >= 0 && *req.ModelIndex < len(cfg.ModelList) {
|
||||
stored := cfg.ModelList[*req.ModelIndex]
|
||||
if stored.APIKey() != "" {
|
||||
m.SetAPIKey(stored.APIKey())
|
||||
}
|
||||
if m.APIBase == "" && stored.APIBase != "" {
|
||||
m.APIBase = stored.APIBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if configuration exists
|
||||
if !hasModelConfiguration(m) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
@ -784,6 +812,11 @@ func (h *Handler) handleFetchModels(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !fetchableProviders[strings.ToLower(req.Provider)] {
|
||||
http.Error(w, fmt.Sprintf("provider %q does not support model listing", req.Provider), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
apiBase := strings.TrimSpace(req.APIBase)
|
||||
if apiBase == "" {
|
||||
apiBase = providers.DefaultAPIBaseForProtocol(req.Provider)
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ export interface TestModelInlineRequest {
|
|||
api_base?: string
|
||||
api_key?: string
|
||||
auth_method?: string
|
||||
model_index?: number
|
||||
}
|
||||
|
||||
export async function testModelInline(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { FetchModelsDialog } from "./fetch-models-dialog"
|
|||
import { type FieldValidation, validateModelField } from "./model-validation"
|
||||
import { ProviderCombobox } from "./provider-combobox"
|
||||
import { getProviderKey } from "./provider-label"
|
||||
import { PROVIDER_MAP } from "./provider-registry"
|
||||
import { FETCHABLE_PROVIDER_KEYS, PROVIDER_MAP } from "./provider-registry"
|
||||
import { TestModelDialog } from "./test-model-dialog"
|
||||
|
||||
interface AddForm {
|
||||
|
|
@ -484,16 +484,17 @@ export function AddModelSheet({
|
|||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setFetchOpen(true)}
|
||||
disabled={!form.provider}
|
||||
>
|
||||
<IconDownload className="size-3" />
|
||||
{t("models.fetch.title")}
|
||||
</Button>
|
||||
{form.provider && FETCHABLE_PROVIDER_KEYS.has(form.provider) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setFetchOpen(true)}
|
||||
>
|
||||
<IconDownload className="size-3" />
|
||||
{t("models.fetch.title")}
|
||||
</Button>
|
||||
)}
|
||||
{!form.provider && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("models.field.selectProviderFirst")}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import { FetchModelsDialog } from "./fetch-models-dialog"
|
|||
import { type FieldValidation, validateModelField } from "./model-validation"
|
||||
import { ProviderCombobox } from "./provider-combobox"
|
||||
import { getProviderKey } from "./provider-label"
|
||||
import { PROVIDER_API_BASES, PROVIDER_MAP } from "./provider-registry"
|
||||
import { FETCHABLE_PROVIDER_KEYS, PROVIDER_API_BASES, PROVIDER_MAP } from "./provider-registry"
|
||||
import { TestModelDialog } from "./test-model-dialog"
|
||||
|
||||
interface EditForm {
|
||||
|
|
@ -441,16 +441,17 @@ export function EditModelSheet({
|
|||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setFetchOpen(true)}
|
||||
disabled={!form.provider}
|
||||
>
|
||||
<IconDownload className="size-3" />
|
||||
{t("models.fetch.title")}
|
||||
</Button>
|
||||
{form.provider && FETCHABLE_PROVIDER_KEYS.has(form.provider) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setFetchOpen(true)}
|
||||
>
|
||||
<IconDownload className="size-3" />
|
||||
{t("models.fetch.title")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
|
|
@ -671,6 +672,7 @@ export function EditModelSheet({
|
|||
apiBase: form.apiBase,
|
||||
apiKey: form.apiKey,
|
||||
authMethod: form.authMethod,
|
||||
modelIndex: model?.index,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export interface ProviderDefinition {
|
|||
priority: number
|
||||
commonModels?: string[]
|
||||
aliases?: string[]
|
||||
/** Whether this provider supports the OpenAI-compatible /models listing endpoint. */
|
||||
supportsFetch?: boolean
|
||||
}
|
||||
|
||||
export const PROVIDERS: ProviderDefinition[] = [
|
||||
|
|
@ -30,6 +32,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
priority: 100,
|
||||
commonModels: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o3-mini"],
|
||||
aliases: ["gpt"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "anthropic",
|
||||
|
|
@ -69,6 +72,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 85,
|
||||
commonModels: ["deepseek-chat", "deepseek-reasoner"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "openrouter",
|
||||
|
|
@ -84,6 +88,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
"anthropic/claude-sonnet-4",
|
||||
"google/gemini-2.0-flash",
|
||||
],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "qwen-portal",
|
||||
|
|
@ -97,6 +102,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
priority: 75,
|
||||
commonModels: ["qwen-max", "qwen-plus", "qwen-turbo"],
|
||||
aliases: ["qwen"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "qwen-intl",
|
||||
|
|
@ -109,6 +115,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
priority: 74,
|
||||
commonModels: ["qwen-max", "qwen-plus", "qwen-turbo"],
|
||||
aliases: ["qwen-international", "dashscope-intl"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "moonshot",
|
||||
|
|
@ -120,6 +127,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 70,
|
||||
commonModels: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "volcengine",
|
||||
|
|
@ -132,6 +140,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 69,
|
||||
commonModels: ["doubao-1.5-pro", "doubao-1.5-lite"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "zhipu",
|
||||
|
|
@ -144,6 +153,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 68,
|
||||
commonModels: ["glm-4-plus", "glm-4-flash"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "groq",
|
||||
|
|
@ -155,6 +165,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 65,
|
||||
commonModels: ["llama-3.3-70b-versatile", "mixtral-8x7b-32768"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "mistral",
|
||||
|
|
@ -166,6 +177,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 64,
|
||||
commonModels: ["mistral-large-latest", "mistral-small-latest"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "nvidia",
|
||||
|
|
@ -177,6 +189,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 63,
|
||||
commonModels: ["meta/llama-3.1-405b-instruct"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "cerebras",
|
||||
|
|
@ -188,6 +201,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 62,
|
||||
commonModels: ["llama3.1-8b", "llama3.1-70b"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "azure",
|
||||
|
|
@ -227,6 +241,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: true,
|
||||
priority: 50,
|
||||
commonModels: ["llama3", "mistral", "codellama", "qwen2.5"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "vllm",
|
||||
|
|
@ -237,6 +252,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: false,
|
||||
isLocal: true,
|
||||
priority: 49,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "lmstudio",
|
||||
|
|
@ -247,6 +263,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: false,
|
||||
isLocal: true,
|
||||
priority: 48,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "venice",
|
||||
|
|
@ -257,6 +274,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 45,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "shengsuanyun",
|
||||
|
|
@ -267,6 +285,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 44,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "vivgrid",
|
||||
|
|
@ -276,6 +295,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 43,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "minimax",
|
||||
|
|
@ -285,6 +305,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 42,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "longcat",
|
||||
|
|
@ -294,6 +315,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 41,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "modelscope",
|
||||
|
|
@ -304,6 +326,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 40,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "mimo",
|
||||
|
|
@ -314,6 +337,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 39,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "avian",
|
||||
|
|
@ -323,6 +347,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 38,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "zai",
|
||||
|
|
@ -333,6 +358,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
isLocal: false,
|
||||
priority: 37,
|
||||
aliases: ["z.ai", "z-ai"],
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "novita",
|
||||
|
|
@ -342,6 +368,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 36,
|
||||
supportsFetch: true,
|
||||
},
|
||||
{
|
||||
key: "litellm",
|
||||
|
|
@ -351,6 +378,7 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
requiresApiKey: true,
|
||||
isLocal: false,
|
||||
priority: 35,
|
||||
supportsFetch: true,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -368,6 +396,10 @@ export const PROVIDER_ALIASES: Record<string, string> = Object.fromEntries(
|
|||
|
||||
export const KNOWN_PROVIDER_KEYS = new Set(PROVIDERS.map((p) => p.key))
|
||||
|
||||
export const FETCHABLE_PROVIDER_KEYS = new Set(
|
||||
PROVIDERS.filter((p) => p.supportsFetch).map((p) => p.key),
|
||||
)
|
||||
|
||||
export const PROVIDER_ICON_SLUGS: Record<string, string> = Object.fromEntries(
|
||||
PROVIDERS.filter((p) => p.iconSlug).map((p) => [p.key, p.iconSlug!]),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface TestInlineParams {
|
|||
apiBase: string
|
||||
apiKey: string
|
||||
authMethod: string
|
||||
modelIndex?: number
|
||||
}
|
||||
|
||||
interface TestModelDialogProps {
|
||||
|
|
@ -62,6 +63,7 @@ export function TestModelDialog({
|
|||
api_base: inlineParams.apiBase || undefined,
|
||||
api_key: inlineParams.apiKey || undefined,
|
||||
auth_method: inlineParams.authMethod || undefined,
|
||||
model_index: inlineParams.modelIndex,
|
||||
}
|
||||
res = await testModelInline(req)
|
||||
} else if (model) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue