From d3351012bf38e84ae27bfc59e37ab1b9605b2515 Mon Sep 17 00:00:00 2001 From: dev-miro26 Date: Mon, 16 Mar 2026 16:32:59 +0000 Subject: [PATCH 1/4] feat: Enhance provider model handling and UI integration --- ..._03_16_1200-add_model_types_to_provider.py | 50 ++++ .../provider/provider_controller.py | 16 +- server/app/model/provider/provider.py | 127 ++++----- src/components/AddWorker/index.tsx | 74 +++++- src/lib/llm.ts | 46 ++++ src/pages/Agents/Models.tsx | 249 +++++++++++++++--- src/types/index.ts | 2 + 7 files changed, 459 insertions(+), 105 deletions(-) create mode 100644 server/alembic/versions/2026_03_16_1200-add_model_types_to_provider.py diff --git a/server/alembic/versions/2026_03_16_1200-add_model_types_to_provider.py b/server/alembic/versions/2026_03_16_1200-add_model_types_to_provider.py new file mode 100644 index 000000000..c4d6ab914 --- /dev/null +++ b/server/alembic/versions/2026_03_16_1200-add_model_types_to_provider.py @@ -0,0 +1,50 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +"""add model_types to provider + +Revision ID: add_model_types_to_provider +Revises: 9464b9d89de7 +Create Date: 2026-03-16 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "add_model_types_to_provider" +down_revision: Union[str, None] = "9464b9d89de7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("provider", sa.Column("model_types", sa.JSON(), nullable=True)) + + # Backfill: copy existing model_type into model_types array + op.execute( + """ + UPDATE provider + SET model_types = json_build_array(model_type) + WHERE model_type IS NOT NULL AND model_type != '' + AND (model_types IS NULL) + """ + ) + + +def downgrade() -> None: + op.drop_column("provider", "model_types") diff --git a/server/app/controller/provider/provider_controller.py b/server/app/controller/provider/provider_controller.py index 6ed32729d..9533370ea 100644 --- a/server/app/controller/provider/provider_controller.py +++ b/server/app/controller/provider/provider_controller.py @@ -73,7 +73,14 @@ async def post(data: ProviderIn, session: Session = Depends(session), auth: Auth """Create a new provider.""" user_id = auth.user.id try: - model = Provider(**data.model_dump(), user_id=user_id) + dump = data.model_dump() + # Ensure model_types always contains model_type for consistency + if dump.get("model_type"): + existing = dump.get("model_types") or [] + if dump["model_type"] not in existing: + existing = [dump["model_type"]] + existing + dump["model_types"] = existing + model = Provider(**dump, user_id=user_id) model.save(session) logger.info( "Provider created", extra={"user_id": user_id, "provider_id": model.id, "provider_name": data.provider_name} @@ -102,6 +109,13 @@ async def put(id: int, data: ProviderIn, session: Session = Depends(session), au model.endpoint_url = data.endpoint_url model.encrypted_config = data.encrypted_config model.is_vaild = data.is_vaild + # Sync model_types: merge incoming list with existing, ensure model_type is included + incoming_types = data.model_types or [] + existing_types = model.model_types or [] + merged = list(dict.fromkeys(incoming_types or existing_types)) + if data.model_type and data.model_type not in merged: + merged = [data.model_type] + merged + model.model_types = merged if merged else None model.save(session) session.refresh(model) logger.info( diff --git a/server/app/model/provider/provider.py b/server/app/model/provider/provider.py index c8545cdeb..b76fc2bce 100644 --- a/server/app/model/provider/provider.py +++ b/server/app/model/provider/provider.py @@ -1,63 +1,66 @@ -# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -from enum import IntEnum - -from pydantic import BaseModel -from sqlalchemy import Boolean, Column, SmallInteger, text -from sqlalchemy_utils import ChoiceType -from sqlmodel import JSON, Field - -from app.model.abstract.model import AbstractModel, DefaultTimes - - -class VaildStatus(IntEnum): - not_valid = 1 - is_valid = 2 - - -class Provider(AbstractModel, DefaultTimes, table=True): - id: int = Field(default=None, primary_key=True) - user_id: int = Field(index=True) - provider_name: str - model_type: str - api_key: str - endpoint_url: str = "" - encrypted_config: dict | None = Field(default=None, sa_column=Column(JSON)) - prefer: bool = Field(default=False, sa_column=Column(Boolean, server_default=text("false"))) - is_vaild: VaildStatus = Field( - default=VaildStatus.not_valid, - sa_column=Column(ChoiceType(VaildStatus, SmallInteger()), server_default=text("1")), - ) - - -class ProviderIn(BaseModel): - provider_name: str - model_type: str - api_key: str - endpoint_url: str - encrypted_config: dict | None = None - is_vaild: VaildStatus = VaildStatus.not_valid - prefer: bool = False - - -class ProviderPreferIn(BaseModel): - provider_id: int - - -class ProviderOut(ProviderIn): - id: int - user_id: int - prefer: bool - model_type: str | None = None +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +from enum import IntEnum + +from pydantic import BaseModel +from sqlalchemy import Boolean, Column, SmallInteger, text +from sqlalchemy_utils import ChoiceType +from sqlmodel import JSON, Field + +from app.model.abstract.model import AbstractModel, DefaultTimes + + +class VaildStatus(IntEnum): + not_valid = 1 + is_valid = 2 + + +class Provider(AbstractModel, DefaultTimes, table=True): + id: int = Field(default=None, primary_key=True) + user_id: int = Field(index=True) + provider_name: str + model_type: str + api_key: str + endpoint_url: str = "" + encrypted_config: dict | None = Field(default=None, sa_column=Column(JSON)) + prefer: bool = Field(default=False, sa_column=Column(Boolean, server_default=text("false"))) + is_vaild: VaildStatus = Field( + default=VaildStatus.not_valid, + sa_column=Column(ChoiceType(VaildStatus, SmallInteger()), server_default=text("1")), + ) + model_types: list[str] | None = Field(default=None, sa_column=Column(JSON)) + + +class ProviderIn(BaseModel): + provider_name: str + model_type: str + api_key: str + endpoint_url: str + encrypted_config: dict | None = None + is_vaild: VaildStatus = VaildStatus.not_valid + prefer: bool = False + model_types: list[str] | None = None + + +class ProviderPreferIn(BaseModel): + provider_id: int + + +class ProviderOut(ProviderIn): + id: int + user_id: int + prefer: bool + model_type: str | None = None + model_types: list[str] | None = None diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 64fc05f49..b1ac349d5 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import { fetchPost } from '@/api/http'; +import { fetchPost, proxyFetchGet } from '@/api/http'; import githubIcon from '@/assets/github.svg'; import { Button } from '@/components/ui/button'; import { @@ -36,7 +36,7 @@ import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { INIT_PROVODERS } from '@/lib/llm'; import { useAuthStore, useWorkerList } from '@/store/authStore'; import { Bot, ChevronDown, ChevronUp, Edit, Eye, EyeOff } from 'lucide-react'; -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import ToolSelect from './ToolSelect'; @@ -109,6 +109,27 @@ export function AddWorker({ const [useCustomModel, setUseCustomModel] = useState(false); const [customModelPlatform, setCustomModelPlatform] = useState(''); const [customModelType, setCustomModelType] = useState(''); + const [savedProviders, setSavedProviders] = useState< + { provider_name: string; model_type: string; model_types: string[] }[] + >([]); + + useEffect(() => { + (async () => { + try { + const res = await proxyFetchGet('/api/providers'); + const list = Array.isArray(res) ? res : res.items || []; + setSavedProviders( + list.map((p: any) => ({ + provider_name: p.provider_name, + model_type: p.model_type || '', + model_types: p.model_types || (p.model_type ? [p.model_type] : []), + })) + ); + } catch { + // ignore + } + })(); + }, [dialogOpen]); if (!chatStore) { return null; @@ -666,16 +687,47 @@ export function AddWorker({ - - setCustomModelType(e.target.value) + {(() => { + const saved = savedProviders.find( + (p) => p.provider_name === customModelPlatform + ); + const models = saved?.model_types || []; + if (models.length > 0) { + return ( + + ); } - /> + return ( + + setCustomModelType(e.target.value) + } + /> + ); + })()} )} diff --git a/src/lib/llm.ts b/src/lib/llm.ts index 65f4340e6..813911484 100644 --- a/src/lib/llm.ts +++ b/src/lib/llm.ts @@ -23,6 +23,12 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Google Gemini model configuration.', is_valid: false, model_type: '', + suggestedModels: [ + 'gemini-2.5-pro-preview-06-05', + 'gemini-2.5-flash-preview-05-20', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ], }, { id: 'openai', @@ -32,6 +38,15 @@ export const INIT_PROVODERS: Provider[] = [ description: 'OpenAI model configuration.', is_valid: false, model_type: '', + suggestedModels: [ + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'o3', + 'o4-mini', + 'gpt-4o', + 'gpt-4o-mini', + ], }, { id: 'anthropic', @@ -41,6 +56,12 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Anthropic Claude API configuration', is_valid: false, model_type: '', + suggestedModels: [ + 'claude-opus-4-20250514', + 'claude-sonnet-4-20250514', + 'claude-3-7-sonnet-20250219', + 'claude-3-5-haiku-20241022', + ], }, { id: 'openrouter', @@ -50,6 +71,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'OpenRouter model configuration.', is_valid: false, model_type: '', + suggestedModels: [], }, { id: 'tongyi-qianwen', @@ -59,6 +81,13 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Qwen model configuration.', is_valid: false, model_type: '', + suggestedModels: [ + 'qwen-max', + 'qwen-plus', + 'qwen-turbo', + 'qwen3-235b-a22b', + 'qwen3-32b', + ], }, { id: 'deepseek', @@ -68,6 +97,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'DeepSeek model configuration.', is_valid: false, model_type: '', + suggestedModels: ['deepseek-chat', 'deepseek-reasoner'], }, { id: 'minimax', @@ -77,6 +107,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Minimax model configuration.', is_valid: false, model_type: '', + suggestedModels: [], }, { id: 'z.ai', @@ -86,6 +117,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Z.ai model configuration.', is_valid: false, model_type: '', + suggestedModels: ['glm-4-plus', 'glm-4-flash', 'glm-4'], }, { id: 'moonshot', @@ -95,6 +127,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Kimi model configuration.', is_valid: false, model_type: '', + suggestedModels: ['moonshot-v1-128k', 'moonshot-v1-32k', 'moonshot-v1-8k'], }, { id: 'ModelArk', @@ -104,6 +137,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'ModelArk model configuration.', is_valid: false, model_type: '', + suggestedModels: [], }, { id: 'samba-nova', @@ -113,6 +147,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'SambaNova model configuration.', is_valid: false, model_type: '', + suggestedModels: [], }, { id: 'grok', @@ -122,6 +157,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Grok model configuration.', is_valid: false, model_type: '', + suggestedModels: ['grok-3', 'grok-3-mini', 'grok-2'], }, { id: 'mistral', @@ -131,6 +167,12 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Mistral model configuration.', is_valid: false, model_type: '', + suggestedModels: [ + 'mistral-large-latest', + 'mistral-medium-latest', + 'mistral-small-latest', + 'codestral-latest', + ], }, { id: 'aws-bedrock', @@ -141,6 +183,7 @@ export const INIT_PROVODERS: Provider[] = [ hostPlaceHolder: 'e.g. https://bedrock-runtime.{{region}}.amazonaws.com', is_valid: false, model_type: '', + suggestedModels: [], }, { id: 'azure', @@ -163,6 +206,7 @@ export const INIT_PROVODERS: Provider[] = [ ], is_valid: false, model_type: '', + suggestedModels: [], }, { id: 'ernie', @@ -172,6 +216,7 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Baidu Ernie model configuration.', is_valid: false, model_type: '', + suggestedModels: ['ernie-4.5-8k', 'ernie-4.0-turbo-8k', 'ernie-3.5-8k'], }, { id: 'openai-compatible-model', @@ -182,5 +227,6 @@ export const INIT_PROVODERS: Provider[] = [ hostPlaceHolder: 'e.g. https://api.x.ai/v1', is_valid: false, model_type: '', + suggestedModels: [], }, ]; diff --git a/src/pages/Agents/Models.tsx b/src/pages/Agents/Models.tsx index 6e5c3ca1b..f938f0e1a 100644 --- a/src/pages/Agents/Models.tsx +++ b/src/pages/Agents/Models.tsx @@ -49,11 +49,14 @@ import { EyeOff, Key, Loader2, + Plus, RotateCcw, Server, Settings, + Star, + X, } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; @@ -97,6 +100,109 @@ import { VLLM_PROVIDER_ID, } from './localModels'; +function ModelTypeInput({ + suggestedModels, + existingModels, + placeholder, + onAdd, +}: { + suggestedModels: string[]; + existingModels: string[]; + placeholder: string; + onAdd: (model: string) => void; +}) { + const [value, setValue] = useState(''); + const [showSuggestions, setShowSuggestions] = useState(false); + const inputRef = useRef(null); + const containerRef = useRef(null); + + const availableSuggestions = suggestedModels.filter( + (m) => + !existingModels.includes(m) && + m.toLowerCase().includes(value.toLowerCase()) + ); + + const handleAdd = () => { + const trimmed = value.trim(); + if (trimmed && !existingModels.includes(trimmed)) { + onAdd(trimmed); + setValue(''); + setShowSuggestions(false); + } + }; + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + setShowSuggestions(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+
+
+ { + setValue(e.target.value); + setShowSuggestions(true); + }} + onFocus={() => setShowSuggestions(true)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAdd(); + } + }} + /> +
+ +
+ {showSuggestions && availableSuggestions.length > 0 && ( +
+
+ {availableSuggestions.map((model) => ( + + ))} +
+
+ )} +
+ ); +} + // Sidebar tab types type SidebarTab = | 'cloud' @@ -134,6 +240,7 @@ export default function SettingModels() { apiHost: p.apiHost, is_valid: p.is_valid ?? false, model_type: p.model_type ?? '', + model_types: [] as string[], externalConfig: p.externalConfig ? p.externalConfig.map((ec) => ({ ...ec })) : undefined, @@ -281,11 +388,13 @@ export default function SettingModels() { ...fi, provider_id: found.id, apiKey: found.api_key || '', - // Fall back to provider's default API host if endpoint_url is empty apiHost: found.endpoint_url || item.apiHost, is_valid: !!found?.is_valid, prefer: found.prefer ?? false, model_type: found.model_type ?? '', + model_types: + found.model_types ?? + (found.model_type ? [found.model_type] : []), externalConfig: fi.externalConfig ? fi.externalConfig.map((ec) => { if ( @@ -387,12 +496,13 @@ export default function SettingModels() { return `${t('setting.eigent-cloud')} / ${modelName}`; } - // Check for custom model preference const preferredIdx = form.findIndex((f) => f.prefer); if (preferredIdx !== -1) { const item = items[preferredIdx]; - const modelType = form[preferredIdx].model_type || ''; - return `${t('setting.custom-model')} / ${item.name}${modelType ? ` (${modelType})` : ''}`; + const activeModel = form[preferredIdx].model_type || ''; + const totalModels = form[preferredIdx].model_types?.length || 0; + const suffix = totalModels > 1 ? ` +${totalModels - 1}` : ''; + return `${t('setting.custom-model')} / ${item.name}${activeModel ? ` (${activeModel}${suffix})` : ''}`; } // Check for local model preference @@ -520,7 +630,10 @@ export default function SettingModels() { } else { newErrors[idx].apiHost = ''; } - if (!model_type || model_type.trim() === '') { + if ( + (!model_type || model_type.trim() === '') && + (!form[idx].model_types || form[idx].model_types.length === 0) + ) { newErrors[idx].model_type = t('setting.model-type-can-not-be-empty'); hasError = true; } else { @@ -587,6 +700,7 @@ export default function SettingModels() { endpoint_url: form[idx].apiHost, is_valid: form[idx].is_valid, model_type: form[idx].model_type, + model_types: form[idx].model_types, }; if (externalConfig) { data.encrypted_config = {}; @@ -614,10 +728,11 @@ export default function SettingModels() { ...fi, provider_id: found.id, apiKey: found.api_key || '', - // Fall back to provider's default API host if endpoint_url is empty apiHost: found.endpoint_url || item.apiHost, is_valid: !!found.is_valid, prefer: found.prefer ?? false, + model_type: found.model_type ?? fi.model_type, + model_types: found.model_types ?? fi.model_types, externalConfig: fi.externalConfig ? fi.externalConfig.map((ec) => { if ( @@ -969,10 +1084,10 @@ export default function SettingModels() { const item = items[i]; return { apiKey: '', - // Restore provider's default API host instead of clearing it apiHost: item.apiHost, is_valid: false, model_type: '', + model_types: [], externalConfig: item.externalConfig ? item.externalConfig.map((ec) => ({ ...ec, value: '' })) : undefined, @@ -1401,29 +1516,101 @@ export default function SettingModels() { ); }} /> - {/* Model Type Setting */} - { - const v = e.target.value; - setForm((f) => - f.map((fi, i) => (i === idx ? { ...fi, model_type: v } : fi)) - ); - setErrors((errs) => - errs.map((er, i) => - i === idx ? { ...er, model_type: '' } : er - ) - ); - }} - /> + {/* Models Management */} +
+
+ {t('setting.model-type-setting')} +
+ {errors[idx]?.model_type && ( + + {errors[idx].model_type} + + )} + {/* Active model indicator + model chips */} + {form[idx].model_types.length > 0 && ( +
+ {form[idx].model_types.map((m) => ( + + {m === form[idx].model_type && ( + + )} + + + + ))} +
+ )} + {/* Add model input with suggestions dropdown */} + { + setForm((f) => + f.map((fi, i) => { + if (i !== idx) return fi; + if (fi.model_types.includes(model)) return fi; + const newTypes = [...fi.model_types, model]; + const newActive = fi.model_type || model; + return { + ...fi, + model_types: newTypes, + model_type: newActive, + }; + }) + ); + setErrors((errs) => + errs.map((er, i) => + i === idx ? { ...er, model_type: '' } : er + ) + ); + }} + /> +
{/* externalConfig render */} {item.externalConfig && form[idx].externalConfig && diff --git a/src/types/index.ts b/src/types/index.ts index feda06ebd..5cf4df1e8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -33,6 +33,8 @@ export type Provider = { externalConfig?: externalConfig[]; is_valid?: boolean; model_type?: string; + model_types?: string[]; + suggestedModels?: string[]; prefer?: boolean; azure_deployment?: string; }; From 2aa6327a6c87ca4b1cf32f342e3e39680319633f Mon Sep 17 00:00:00 2001 From: dev-miro26 Date: Wed, 18 Mar 2026 14:04:15 -0600 Subject: [PATCH 2/4] feat: update model suggestions in llm.ts and improve model type handling in Models.tsx --- src/lib/llm.ts | 146 ++++++++++++++++++++++++++++++++---- src/pages/Agents/Models.tsx | 20 ++--- 2 files changed, 138 insertions(+), 28 deletions(-) diff --git a/src/lib/llm.ts b/src/lib/llm.ts index 813911484..f3857d007 100644 --- a/src/lib/llm.ts +++ b/src/lib/llm.ts @@ -24,8 +24,12 @@ export const INIT_PROVODERS: Provider[] = [ is_valid: false, model_type: '', suggestedModels: [ - 'gemini-2.5-pro-preview-06-05', - 'gemini-2.5-flash-preview-05-20', + 'gemini-3.1-pro-preview', + 'gemini-3-flash-preview', + 'gemini-3.1-flash-lite-preview', + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.5-flash-lite', 'gemini-2.0-flash', 'gemini-2.0-flash-lite', ], @@ -39,10 +43,17 @@ export const INIT_PROVODERS: Provider[] = [ is_valid: false, model_type: '', suggestedModels: [ + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.4-nano', + 'gpt-5', + 'gpt-5-mini', + 'gpt-5-nano', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'o3', + 'o3-mini', 'o4-mini', 'gpt-4o', 'gpt-4o-mini', @@ -57,10 +68,14 @@ export const INIT_PROVODERS: Provider[] = [ is_valid: false, model_type: '', suggestedModels: [ + 'claude-opus-4-6', + 'claude-sonnet-4-6', + 'claude-opus-4-5-20251101', + 'claude-sonnet-4-5-20250929', + 'claude-opus-4-1-20250805', 'claude-opus-4-20250514', 'claude-sonnet-4-20250514', - 'claude-3-7-sonnet-20250219', - 'claude-3-5-haiku-20241022', + 'claude-haiku-4-5-20251001', ], }, { @@ -71,7 +86,27 @@ export const INIT_PROVODERS: Provider[] = [ description: 'OpenRouter model configuration.', is_valid: false, model_type: '', - suggestedModels: [], + suggestedModels: [ + 'anthropic/claude-sonnet-4-6', + 'anthropic/claude-sonnet-4', + 'openai/gpt-5.4', + 'openai/gpt-5.4-mini', + 'openai/gpt-5.4-nano', + 'openai/gpt-4.1', + 'openai/o3', + 'openai/o4-mini', + 'google/gemini-3.1-pro-preview', + 'google/gemini-3-flash-preview', + 'google/gemini-2.5-pro', + 'google/gemini-2.5-flash', + 'deepseek/deepseek-v3.2', + 'deepseek/deepseek-r1', + 'minimax/minimax-m2.5', + 'meta-llama/llama-4-maverick', + 'mistralai/mistral-large', + 'qwen/qwen3-235b-a22b', + 'x-ai/grok-4', + ], }, { id: 'tongyi-qianwen', @@ -87,6 +122,12 @@ export const INIT_PROVODERS: Provider[] = [ 'qwen-turbo', 'qwen3-235b-a22b', 'qwen3-32b', + 'qwen3-14b', + 'qwen3-8b', + 'qwen3-4b', + 'qwen3-1.7b', + 'qwen3-0.6b', + 'qwq-plus', ], }, { @@ -107,7 +148,15 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Minimax model configuration.', is_valid: false, model_type: '', - suggestedModels: [], + suggestedModels: [ + 'MiniMax-M2.7', + 'MiniMax-M2.7-highspeed', + 'MiniMax-M2.5', + 'MiniMax-M2.5-highspeed', + 'MiniMax-M2.1', + 'MiniMax-M2.1-highspeed', + 'MiniMax-M2', + ], }, { id: 'z.ai', @@ -117,7 +166,13 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Z.ai model configuration.', is_valid: false, model_type: '', - suggestedModels: ['glm-4-plus', 'glm-4-flash', 'glm-4'], + suggestedModels: [ + 'glm-4.7', + 'glm-4.6', + 'glm-4-plus', + 'glm-4-flash', + 'glm-4', + ], }, { id: 'moonshot', @@ -127,7 +182,13 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Kimi model configuration.', is_valid: false, model_type: '', - suggestedModels: ['moonshot-v1-128k', 'moonshot-v1-32k', 'moonshot-v1-8k'], + suggestedModels: [ + 'kimi-k2.5', + 'kimi-k2', + 'moonshot-v1-128k', + 'moonshot-v1-32k', + 'moonshot-v1-8k', + ], }, { id: 'ModelArk', @@ -137,7 +198,12 @@ export const INIT_PROVODERS: Provider[] = [ description: 'ModelArk model configuration.', is_valid: false, model_type: '', - suggestedModels: [], + suggestedModels: [ + 'doubao-seed-2.0-pro', + 'doubao-seed-2.0-lite', + 'doubao-seed-2.0-mini', + 'doubao-seed-2.0-code', + ], }, { id: 'samba-nova', @@ -147,7 +213,18 @@ export const INIT_PROVODERS: Provider[] = [ description: 'SambaNova model configuration.', is_valid: false, model_type: '', - suggestedModels: [], + suggestedModels: [ + 'DeepSeek-V3.1', + 'DeepSeek-V3-0324', + 'DeepSeek-R1-0528', + 'DeepSeek-R1-Distill-Llama-70B', + 'Meta-Llama-3.3-70B-Instruct', + 'Meta-Llama-3.1-8B-Instruct', + 'Llama-4-Maverick-17B-128E-Instruct', + 'Llama-4-Scout-17B-16E-Instruct', + 'Qwen3-235B-A22B-Instruct-2507', + 'Qwen3-32B', + ], }, { id: 'grok', @@ -157,7 +234,14 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Grok model configuration.', is_valid: false, model_type: '', - suggestedModels: ['grok-3', 'grok-3-mini', 'grok-2'], + suggestedModels: [ + 'grok-4', + 'grok-3', + 'grok-3-mini', + 'grok-3-fast', + 'grok-code-fast-1', + 'grok-2', + ], }, { id: 'mistral', @@ -172,6 +256,8 @@ export const INIT_PROVODERS: Provider[] = [ 'mistral-medium-latest', 'mistral-small-latest', 'codestral-latest', + 'devstral-latest', + 'ministral-8b-latest', ], }, { @@ -183,7 +269,18 @@ export const INIT_PROVODERS: Provider[] = [ hostPlaceHolder: 'e.g. https://bedrock-runtime.{{region}}.amazonaws.com', is_valid: false, model_type: '', - suggestedModels: [], + suggestedModels: [ + 'anthropic.claude-opus-4-6-v1', + 'anthropic.claude-sonnet-4-6', + 'anthropic.claude-sonnet-4-5-20250929-v1:0', + 'anthropic.claude-sonnet-4-20250514-v1:0', + 'anthropic.claude-haiku-4-5-20251001-v1:0', + 'us.anthropic.claude-opus-4-6-v1', + 'us.anthropic.claude-sonnet-4-6', + 'us.meta.llama4-maverick-17b-instruct-v1:0', + 'us.meta.llama4-scout-17b-instruct-v1:0', + 'mistral.mistral-large-2407-v1:0', + ], }, { id: 'azure', @@ -206,7 +303,20 @@ export const INIT_PROVODERS: Provider[] = [ ], is_valid: false, model_type: '', - suggestedModels: [], + suggestedModels: [ + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.4-nano', + 'gpt-5', + 'gpt-5-mini', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-4o', + 'gpt-4o-mini', + 'o3', + 'o4-mini', + ], }, { id: 'ernie', @@ -216,7 +326,15 @@ export const INIT_PROVODERS: Provider[] = [ description: 'Baidu Ernie model configuration.', is_valid: false, model_type: '', - suggestedModels: ['ernie-4.5-8k', 'ernie-4.0-turbo-8k', 'ernie-3.5-8k'], + suggestedModels: [ + 'ernie-5.0', + 'ernie-5.0-thinking-preview', + 'ernie-4.5-8k', + 'ernie-4.5-turbo-128k', + 'ernie-4.0-turbo-8k', + 'ernie-x1', + 'ernie-3.5-8k', + ], }, { id: 'openai-compatible-model', diff --git a/src/pages/Agents/Models.tsx b/src/pages/Agents/Models.tsx index a30b23905..8eedcdd22 100644 --- a/src/pages/Agents/Models.tsx +++ b/src/pages/Agents/Models.tsx @@ -116,15 +116,13 @@ function ModelTypeInput({ const inputRef = useRef(null); const containerRef = useRef(null); - const availableSuggestions = suggestedModels.filter( - (m) => - !existingModels.includes(m) && - m.toLowerCase().includes(value.toLowerCase()) + const availableSuggestions = suggestedModels.filter((m) => + m.toLowerCase().includes(value.toLowerCase()) ); const handleAdd = () => { const trimmed = value.trim(); - if (trimmed && !existingModels.includes(trimmed)) { + if (trimmed) { onAdd(trimmed); setValue(''); setShowSuggestions(false); @@ -190,7 +188,6 @@ function ModelTypeInput({ onAdd(model); setValue(''); setShowSuggestions(false); - inputRef.current?.focus(); }} > {model} @@ -500,9 +497,7 @@ export default function SettingModels() { if (preferredIdx !== -1) { const item = items[preferredIdx]; const activeModel = form[preferredIdx].model_type || ''; - const totalModels = form[preferredIdx].model_types?.length || 0; - const suffix = totalModels > 1 ? ` +${totalModels - 1}` : ''; - return `${t('setting.custom-model')} / ${item.name}${activeModel ? ` (${activeModel}${suffix})` : ''}`; + return `${t('setting.custom-model')} / ${item.name}${activeModel ? ` (${activeModel})` : ''}`; } // Check for local model preference @@ -1597,13 +1592,10 @@ export default function SettingModels() { setForm((f) => f.map((fi, i) => { if (i !== idx) return fi; - if (fi.model_types.includes(model)) return fi; - const newTypes = [...fi.model_types, model]; - const newActive = fi.model_type || model; return { ...fi, - model_types: newTypes, - model_type: newActive, + model_types: [model], + model_type: model, }; }) ); From 4e9491476e7f1461b5f25b0a22e38febb84c1fe0 Mon Sep 17 00:00:00 2001 From: dev-miro26 Date: Tue, 24 Mar 2026 13:08:02 -0600 Subject: [PATCH 3/4] feat(provider): add endpoint to retrieve models for a provider --- .../domains/model_provider/api/provider_controller.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/app/domains/model_provider/api/provider_controller.py b/server/app/domains/model_provider/api/provider_controller.py index c28664db7..6acdf5435 100644 --- a/server/app/domains/model_provider/api/provider_controller.py +++ b/server/app/domains/model_provider/api/provider_controller.py @@ -68,6 +68,15 @@ async def put(id: int, data: ProviderIn, auth: V1UserAuth = Depends(auth_must)): return result["provider"] +@router.get("/provider/{id}/models", name="get provider models") +async def get_models(id: int, auth: V1UserAuth = Depends(auth_must)): + """Get the list of sub-models for a provider.""" + model = ProviderService.get(id, auth.id) + if not model: + raise HTTPException(status_code=404, detail=_("Provider not found")) + return {"models": model.model_types or []} + + @router.delete("/provider/{id}", name="delete provider") async def delete(id: int, auth: V1UserAuth = Depends(auth_must)): if not ProviderService.delete(id, auth.id): From 2de10b625a91909ccd2cb7f92e6bcd78e36f1b33 Mon Sep 17 00:00:00 2001 From: dev-miro26 Date: Tue, 24 Mar 2026 14:30:18 -0600 Subject: [PATCH 4/4] feat(provider): expand updatable fields and improve model handling in UI --- .../service/provider_service.py | 5 +- src/pages/Agents/Models.tsx | 90 ++++++++++++------- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/server/app/domains/model_provider/service/provider_service.py b/server/app/domains/model_provider/service/provider_service.py index 599d3f728..3f868e4d4 100644 --- a/server/app/domains/model_provider/service/provider_service.py +++ b/server/app/domains/model_provider/service/provider_service.py @@ -66,7 +66,10 @@ def update(provider_id: int, user_id: int, data: dict) -> dict: if not model: return {"success": False, "error_code": "PROVIDER_NOT_FOUND"} # H10: only allow updating safe fields - _UPDATABLE_FIELDS = {"provider_name", "api_key", "api_base", "extra_config", "prefer", "is_vaild"} + _UPDATABLE_FIELDS = { + "provider_name", "api_key", "endpoint_url", "encrypted_config", + "model_type", "model_types", "prefer", "is_vaild", + } for key, value in data.items(): if key in _UPDATABLE_FIELDS: setattr(model, key, value) diff --git a/src/pages/Agents/Models.tsx b/src/pages/Agents/Models.tsx index 1cf977b17..40c1a560a 100644 --- a/src/pages/Agents/Models.tsx +++ b/src/pages/Agents/Models.tsx @@ -17,7 +17,6 @@ import { proxyFetchDelete, proxyFetchGet, proxyFetchPost, - proxyFetchPut, } from '@/api/http'; import { Button } from '@/components/ui/button'; import { @@ -386,7 +385,7 @@ export default function SettingModels() { provider_id: found.id, apiKey: found.api_key || '', apiHost: found.endpoint_url || item.apiHost, - is_valid: !!found?.is_valid, + is_valid: found?.is_vaild === 2, prefer: found.prefer ?? false, model_type: found.model_type ?? '', model_types: @@ -454,19 +453,6 @@ export default function SettingModels() { setLocalTypes(types); setLocalProviderIds(providerIds); } - if (modelType === 'cloud') { - setCloudPrefer(true); - setForm((f) => f.map((fi) => ({ ...fi, prefer: false }))); - setLocalPrefer(false); - } else if (modelType === 'local') { - setLocalEnabled(true); - setForm((f) => f.map((fi) => ({ ...fi, prefer: false }))); - setLocalPrefer(true); - setCloudPrefer(false); - } else { - setLocalPrefer(false); - setCloudPrefer(false); - } } catch (e) { console.error('Error fetching providers:', e); // ignore error @@ -477,7 +463,21 @@ export default function SettingModels() { fetchSubscription(); updateCredits(); } - }, [items, modelType, fetchModelsForPlatform]); + }, [items, fetchModelsForPlatform]); + + // Sync prefer UI flags when modelType changes + useEffect(() => { + if (modelType === 'cloud') { + setCloudPrefer(true); + setForm((f) => f.map((fi) => ({ ...fi, prefer: false }))); + setLocalPrefer(false); + } else if (modelType === 'local') { + setLocalEnabled(true); + setForm((f) => f.map((fi) => ({ ...fi, prefer: false }))); + setLocalPrefer(true); + setCloudPrefer(false); + } + }, [modelType]); // Get current default model display text const getDefaultModelDisplayText = (): string => { @@ -694,7 +694,7 @@ export default function SettingModels() { provider_name: item.id, api_key: form[idx].apiKey, endpoint_url: form[idx].apiHost, - is_valid: form[idx].is_valid, + is_vaild: 2, model_type: form[idx].model_type, model_types: form[idx].model_types, }; @@ -704,31 +704,49 @@ export default function SettingModels() { data.encrypted_config[ec.key] = ec.value; }); } + const savedModelType = form[idx].model_type; + const savedModelTypes = form[idx].model_types; try { if (provider_id) { - await proxyFetchPut(`/api/v1/provider/${provider_id}`, data); - } else { - await proxyFetchPost('/api/v1/provider', data); + // DELETE + POST to work around remote server's _UPDATABLE_FIELDS + // not including model_type / model_types in PUT + await proxyFetchDelete(`/api/v1/provider/${provider_id}`); } - // add: refresh provider list after saving, update form and switch editable status + const created = await proxyFetchPost('/api/v1/provider', data); + const newProviderId = created?.id; + + // Always set this provider as preferred using the correct new ID + if (newProviderId) { + await proxyFetchPost('/api/v1/provider/prefer', { + provider_id: newProviderId, + }); + } + + // Refresh provider list after saving const res = await proxyFetchGet('/api/v1/providers'); const providerList = Array.isArray(res) ? res : res.items || []; + setForm((f) => f.map((fi, i) => { - const item = items[i]; + const curItem = items[i]; const found = providerList.find( - (p: any) => p.provider_name === item.id + (p: any) => p.provider_name === curItem.id ); if (found) { + const isCurrentProvider = i === idx; return { ...fi, provider_id: found.id, apiKey: found.api_key || '', - apiHost: found.endpoint_url || item.apiHost, - is_valid: !!found.is_valid, - prefer: found.prefer ?? false, - model_type: found.model_type ?? fi.model_type, - model_types: found.model_types ?? fi.model_types, + apiHost: found.endpoint_url || curItem.apiHost, + is_valid: found?.is_vaild === 2, + prefer: isCurrentProvider ? true : false, + model_type: isCurrentProvider + ? savedModelType + : (found.model_type ?? fi.model_type), + model_types: isCurrentProvider + ? savedModelTypes + : (found.model_types ?? fi.model_types), externalConfig: fi.externalConfig ? fi.externalConfig.map((ec) => { if ( @@ -746,16 +764,19 @@ export default function SettingModels() { }) ); - // Check if this was a pending default model selection + // Update UI state directly (avoid handleSwitch which reads stale form state) + setModelType('custom'); + setActiveModelIdx(idx); + setLocalEnabled(false); + setCloudPrefer(false); + setLocalPrefer(false); + if ( pendingDefaultModel && pendingDefaultModel.category === 'custom' && pendingDefaultModel.modelId === item.id ) { - await handleSwitch(idx, true); setPendingDefaultModel(null); - } else { - handleSwitch(idx, true); } } finally { setLoading(null); @@ -900,9 +921,10 @@ export default function SettingModels() { }, }; - // Update or create provider + // DELETE + POST to work around remote server ignoring model_type in PUT if (currentProviderId) { - await proxyFetchPut(`/api/v1/provider/${currentProviderId}`, data); + await proxyFetchDelete(`/api/v1/provider/${currentProviderId}`); + await proxyFetchPost('/api/v1/provider', data); } else { await proxyFetchPost('/api/v1/provider', data); }