Skip to content
Original file line number Diff line number Diff line change
@@ -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")
16 changes: 15 additions & 1 deletion server/app/controller/provider/provider_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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(
Expand Down
127 changes: 65 additions & 62 deletions server/app/model/provider/provider.py
Original file line number Diff line number Diff line change
@@ -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
74 changes: 63 additions & 11 deletions src/components/AddWorker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -666,16 +687,47 @@ export function AddWorker({
<label className="text-xs text-text-body">
{t('workforce.model-type')}
</label>
<Input
size="sm"
placeholder={t(
'workforce.model-type-placeholder'
)}
value={customModelType}
onChange={(e) =>
setCustomModelType(e.target.value)
{(() => {
const saved = savedProviders.find(
(p) => p.provider_name === customModelPlatform
);
const models = saved?.model_types || [];
if (models.length > 0) {
return (
<Select
value={customModelType}
onValueChange={setCustomModelType}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
'workforce.model-type-placeholder'
)}
/>
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m} value={m}>
{m}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
/>
return (
<Input
size="sm"
placeholder={t(
'workforce.model-type-placeholder'
)}
value={customModelType}
onChange={(e) =>
setCustomModelType(e.target.value)
}
/>
);
})()}
</div>
</>
)}
Expand Down
Loading
Loading