mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor: support explicit provider field in model list entries (#2609)
* refactor: support explicit model list providers * fix(web): preserve explicit model providers * fix(web): preserve legacy provider prefixes on model updates fix(models): normalize explicit provider-prefixed ids fix(api): preserve legacy model updates across providers fix(agent): preserve config identity for explicit provider refs * fix ci
This commit is contained in:
@@ -6,6 +6,7 @@ import { refreshGatewayState } from "@/store/gateway"
|
||||
export interface ModelInfo {
|
||||
index: number
|
||||
model_name: string
|
||||
provider?: string
|
||||
model: string
|
||||
api_base?: string
|
||||
api_key: string
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
interface AddForm {
|
||||
modelName: string
|
||||
provider: string
|
||||
model: string
|
||||
apiBase: string
|
||||
apiKey: string
|
||||
@@ -41,6 +42,7 @@ interface AddForm {
|
||||
|
||||
const EMPTY_ADD_FORM: AddForm = {
|
||||
modelName: "",
|
||||
provider: "",
|
||||
model: "",
|
||||
apiBase: "",
|
||||
apiKey: "",
|
||||
@@ -119,9 +121,11 @@ export function AddModelSheet({
|
||||
setServerError("")
|
||||
try {
|
||||
const modelName = form.modelName.trim()
|
||||
const provider = form.provider.trim()
|
||||
const modelId = form.model.trim()
|
||||
await addModel({
|
||||
model_name: modelName,
|
||||
provider: provider || undefined,
|
||||
model: modelId,
|
||||
api_base: form.apiBase.trim() || undefined,
|
||||
api_key: form.apiKey.trim() || undefined,
|
||||
@@ -186,6 +190,17 @@ export function AddModelSheet({
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={t("models.field.provider")}
|
||||
hint={t("models.field.providerHint")}
|
||||
>
|
||||
<Input
|
||||
value={form.provider}
|
||||
onChange={setField("provider")}
|
||||
placeholder={t("models.field.providerPlaceholder")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={t("models.add.modelId")}
|
||||
hint={t("models.add.modelIdHint")}
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
interface EditForm {
|
||||
provider: string
|
||||
modelId: string
|
||||
apiKey: string
|
||||
apiBase: string
|
||||
proxy: string
|
||||
@@ -52,6 +54,8 @@ export function EditModelSheet({
|
||||
}: EditModelSheetProps) {
|
||||
const { t } = useTranslation()
|
||||
const [form, setForm] = useState<EditForm>({
|
||||
provider: "",
|
||||
modelId: "",
|
||||
apiKey: "",
|
||||
apiBase: "",
|
||||
proxy: "",
|
||||
@@ -72,6 +76,8 @@ export function EditModelSheet({
|
||||
useEffect(() => {
|
||||
if (model) {
|
||||
setForm({
|
||||
provider: model.provider ?? "",
|
||||
modelId: model.model,
|
||||
apiKey: "",
|
||||
apiBase: model.api_base ?? "",
|
||||
proxy: model.proxy ?? "",
|
||||
@@ -103,12 +109,17 @@ export function EditModelSheet({
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!model) return
|
||||
if (!form.modelId.trim()) {
|
||||
setError(t("models.add.errorRequired"))
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
setError("")
|
||||
try {
|
||||
await updateModel(model.index, {
|
||||
model_name: model.model_name,
|
||||
model: model.model,
|
||||
provider: form.provider.trim(),
|
||||
model: form.modelId.trim(),
|
||||
api_base: form.apiBase || undefined,
|
||||
api_key: form.apiKey || undefined,
|
||||
proxy: form.proxy || undefined,
|
||||
@@ -166,6 +177,29 @@ export function EditModelSheet({
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="space-y-5 px-6 py-5">
|
||||
<Field
|
||||
label={t("models.field.provider")}
|
||||
hint={t("models.field.providerHint")}
|
||||
>
|
||||
<Input
|
||||
value={form.provider}
|
||||
onChange={setField("provider")}
|
||||
placeholder={t("models.field.providerPlaceholder")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={t("models.add.modelId")}
|
||||
hint={t("models.add.modelIdHint")}
|
||||
>
|
||||
<Input
|
||||
value={form.modelId}
|
||||
onChange={setField("modelId")}
|
||||
placeholder={t("models.add.modelIdPlaceholder")}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{!isOAuth && (
|
||||
<Field
|
||||
label={t("models.field.apiKey")}
|
||||
|
||||
@@ -20,19 +20,28 @@ const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
zhipu: 4,
|
||||
deepseek: 5,
|
||||
openrouter: 6,
|
||||
qwen: 7,
|
||||
moonshot: 8,
|
||||
groq: 9,
|
||||
"github-copilot": 10,
|
||||
antigravity: 11,
|
||||
nvidia: 12,
|
||||
cerebras: 13,
|
||||
shengsuanyun: 14,
|
||||
ollama: 15,
|
||||
vllm: 16,
|
||||
mistral: 17,
|
||||
avian: 18,
|
||||
mimo: 19,
|
||||
"qwen-portal": 7,
|
||||
"qwen-intl": 8,
|
||||
moonshot: 9,
|
||||
groq: 10,
|
||||
"github-copilot": 11,
|
||||
antigravity: 12,
|
||||
nvidia: 13,
|
||||
cerebras: 14,
|
||||
shengsuanyun: 15,
|
||||
venice: 16,
|
||||
vivgrid: 17,
|
||||
minimax: 18,
|
||||
longcat: 19,
|
||||
modelscope: 20,
|
||||
mistral: 21,
|
||||
avian: 22,
|
||||
azure: 23,
|
||||
ollama: 24,
|
||||
vllm: 25,
|
||||
lmstudio: 26,
|
||||
zai: 27,
|
||||
mimo: 28,
|
||||
}
|
||||
|
||||
interface ProviderGroup {
|
||||
@@ -95,10 +104,10 @@ export function ModelsPage() {
|
||||
|
||||
const grouped: Record<string, { label: string; models: ModelInfo[] }> = {}
|
||||
for (const model of models) {
|
||||
const providerKey = getProviderKey(model.model)
|
||||
const providerKey = getProviderKey(model.provider)
|
||||
if (!grouped[providerKey]) {
|
||||
grouped[providerKey] = {
|
||||
label: getProviderLabel(model.model),
|
||||
label: getProviderLabel(model.provider),
|
||||
models: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import { useMemo, useState } from "react"
|
||||
const PROVIDER_ICON_SLUGS: Record<string, string> = {
|
||||
openai: "openai",
|
||||
anthropic: "anthropic",
|
||||
azure: "microsoftazure",
|
||||
gemini: "googlegemini",
|
||||
deepseek: "deepseek",
|
||||
qwen: "alibabacloud",
|
||||
"qwen-portal": "alibabacloud",
|
||||
"qwen-intl": "alibabacloud",
|
||||
groq: "groq",
|
||||
openrouter: "openrouter",
|
||||
nvidia: "nvidia",
|
||||
@@ -20,9 +22,11 @@ const PROVIDER_ICON_SLUGS: Record<string, string> = {
|
||||
const PROVIDER_DOMAINS: Record<string, string> = {
|
||||
openai: "openai.com",
|
||||
anthropic: "anthropic.com",
|
||||
azure: "azure.com",
|
||||
gemini: "gemini.google.com",
|
||||
deepseek: "deepseek.com",
|
||||
qwen: "qwenlm.ai",
|
||||
"qwen-portal": "qwenlm.ai",
|
||||
"qwen-intl": "alibabacloud.com",
|
||||
moonshot: "moonshot.ai",
|
||||
groq: "groq.com",
|
||||
openrouter: "openrouter.ai",
|
||||
@@ -33,11 +37,18 @@ const PROVIDER_DOMAINS: Record<string, string> = {
|
||||
antigravity: "antigravity.google",
|
||||
"github-copilot": "github.com",
|
||||
ollama: "ollama.com",
|
||||
lmstudio: "lmstudio.ai",
|
||||
mistral: "mistral.ai",
|
||||
avian: "avian.io",
|
||||
vllm: "vllm.ai",
|
||||
zhipu: "zhipuai.cn",
|
||||
zai: "z.ai",
|
||||
mimo: "xiaomi.com",
|
||||
venice: "venice.ai",
|
||||
vivgrid: "vivgrid.com",
|
||||
minimax: "minimaxi.com",
|
||||
longcat: "longcat.chat",
|
||||
modelscope: "modelscope.cn",
|
||||
}
|
||||
|
||||
interface ProviderIconProps {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const PROVIDER_LABELS: Record<string, string> = {
|
||||
openai: "OpenAI",
|
||||
anthropic: "Anthropic",
|
||||
azure: "Azure OpenAI",
|
||||
gemini: "Google Gemini",
|
||||
deepseek: "DeepSeek",
|
||||
qwen: "Qwen (阿里云)",
|
||||
"qwen-portal": "Qwen (阿里云)",
|
||||
"qwen-intl": "Qwen International",
|
||||
moonshot: "Moonshot (月之暗面)",
|
||||
groq: "Groq",
|
||||
openrouter: "OpenRouter",
|
||||
@@ -14,21 +16,37 @@ const PROVIDER_LABELS: Record<string, string> = {
|
||||
antigravity: "Google Code Assist",
|
||||
"github-copilot": "GitHub Copilot",
|
||||
ollama: "Ollama (local)",
|
||||
lmstudio: "LM Studio (local)",
|
||||
mistral: "Mistral AI",
|
||||
avian: "Avian",
|
||||
vllm: "VLLM (local)",
|
||||
zhipu: "Zhipu AI (智谱)",
|
||||
zai: "Z.ai",
|
||||
mimo: "Xiaomi MiMo",
|
||||
venice: "Venice AI",
|
||||
vivgrid: "Vivgrid",
|
||||
minimax: "MiniMax",
|
||||
longcat: "LongCat",
|
||||
modelscope: "ModelScope (魔搭社区)",
|
||||
}
|
||||
|
||||
export function getProviderKey(model: string): string {
|
||||
return model.split("/")[0]
|
||||
const PROVIDER_ALIASES: Record<string, string> = {
|
||||
qwen: "qwen-portal",
|
||||
"qwen-international": "qwen-intl",
|
||||
"dashscope-intl": "qwen-intl",
|
||||
"z.ai": "zai",
|
||||
"z-ai": "zai",
|
||||
google: "gemini",
|
||||
"google-antigravity": "antigravity",
|
||||
}
|
||||
|
||||
export function getProviderLabel(model: string): string {
|
||||
const prefix = getProviderKey(model)
|
||||
const labels: Record<string, string> = {
|
||||
...PROVIDER_LABELS,
|
||||
}
|
||||
return labels[prefix] ?? prefix
|
||||
export function getProviderKey(provider?: string): string {
|
||||
const normalized = provider?.trim().toLowerCase()
|
||||
if (!normalized) return "openai"
|
||||
return PROVIDER_ALIASES[normalized] ?? normalized
|
||||
}
|
||||
|
||||
export function getProviderLabel(provider?: string): string {
|
||||
const prefix = getProviderKey(provider)
|
||||
return PROVIDER_LABELS[prefix] ?? prefix
|
||||
}
|
||||
|
||||
@@ -244,8 +244,8 @@
|
||||
"modelNamePlaceholder": "e.g. my-gpt4",
|
||||
"modelNameHint": "A short name used to identify this model in conversations.",
|
||||
"modelId": "Model Identifier",
|
||||
"modelIdPlaceholder": "e.g. openai/gpt-4o",
|
||||
"modelIdHint": "Format: protocol/model-id. Supported: openai, anthropic, gemini, groq, …",
|
||||
"modelIdPlaceholder": "e.g. gpt-4o or openai/gpt-4o",
|
||||
"modelIdHint": "If Provider is not specified, values such as openai/gpt-4o are interpreted using the provider/model format. If Provider is specified, this field is treated as the canonical model ID and is not parsed for a provider prefix.",
|
||||
"errorRequired": "This field is required.",
|
||||
"errorDuplicateModelName": "Model alias already exists. Please use a different name.",
|
||||
"saveError": "Failed to add model",
|
||||
@@ -260,6 +260,9 @@
|
||||
"toggle": "Advanced options"
|
||||
},
|
||||
"field": {
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "e.g. openai",
|
||||
"providerHint": "Optional. If specified, this value is used as the effective provider, and Model Identifier is interpreted as the canonical model ID.",
|
||||
"apiBase": "API Base URL",
|
||||
"apiKey": "API Key",
|
||||
"apiKeyPlaceholder": "Enter your API key",
|
||||
|
||||
@@ -244,8 +244,8 @@
|
||||
"modelNamePlaceholder": "例如 my-gpt4",
|
||||
"modelNameHint": "用于在对话中识别此模型的简短名称。",
|
||||
"modelId": "模型标识符",
|
||||
"modelIdPlaceholder": "例如 openai/gpt-4o",
|
||||
"modelIdHint": "格式:协议/模型ID。支持:openai、anthropic、gemini、groq 等。",
|
||||
"modelIdPlaceholder": "例如 gpt-4o 或 openai/gpt-4o",
|
||||
"modelIdHint": "未指定 Provider 时,诸如 openai/gpt-4o 的值将按 provider/model 格式解析。已指定 Provider 时,此字段将作为规范模型 ID 使用,不再解析其中的 provider 前缀。",
|
||||
"errorRequired": "此字段为必填项。",
|
||||
"errorDuplicateModelName": "模型别名已存在,请使用其他名称。",
|
||||
"saveError": "添加模型失败",
|
||||
@@ -260,6 +260,9 @@
|
||||
"toggle": "高级选项"
|
||||
},
|
||||
"field": {
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "例如 openai",
|
||||
"providerHint": "可选。指定后,将以该值作为最终 provider,并将“模型标识符”字段解释为规范模型 ID。",
|
||||
"apiBase": "API Base URL",
|
||||
"apiKey": "API Key",
|
||||
"apiKeyPlaceholder": "请输入 API Key",
|
||||
|
||||
Reference in New Issue
Block a user