mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-07-09 17:28:35 +00:00
Merge d0925881b0 into 7521db5398
This commit is contained in:
commit
0c84435e20
4 changed files with 108 additions and 24 deletions
|
|
@ -14,9 +14,9 @@
|
|||
|
||||
"""ProviderService: model provider CRUD with prefer/invalidate. Follows CreditsService pattern."""
|
||||
|
||||
from sqlalchemy import update
|
||||
from sqlmodel import select, col
|
||||
from loguru import logger
|
||||
from sqlalchemy import update
|
||||
from sqlmodel import col, select
|
||||
|
||||
from app.core.database import session_make
|
||||
from app.model.provider.provider import Provider, VaildStatus
|
||||
|
|
@ -66,7 +66,15 @@ class ProviderService:
|
|||
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",
|
||||
"prefer",
|
||||
"is_vaild",
|
||||
}
|
||||
for key, value in data.items():
|
||||
if key in _UPDATABLE_FIELDS:
|
||||
setattr(model, key, value)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
# ========= 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. =========
|
||||
# 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 typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, model_validator
|
||||
from sqlalchemy import Boolean, Column, SmallInteger, text
|
||||
from sqlalchemy_utils import ChoiceType
|
||||
from sqlmodel import JSON, Field
|
||||
|
|
@ -51,6 +52,25 @@ class ProviderIn(BaseModel):
|
|||
is_vaild: VaildStatus = VaildStatus.not_valid
|
||||
prefer: bool = False
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def normalize_valid_field(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
if "is_vaild" in data or "is_valid" not in data:
|
||||
return data
|
||||
data = dict(data)
|
||||
is_valid = data.pop("is_valid")
|
||||
if isinstance(is_valid, bool):
|
||||
data["is_vaild"] = (
|
||||
VaildStatus.is_valid
|
||||
if is_valid
|
||||
else VaildStatus.not_valid
|
||||
)
|
||||
else:
|
||||
data["is_vaild"] = is_valid
|
||||
return data
|
||||
|
||||
|
||||
class ProviderPreferIn(BaseModel):
|
||||
provider_id: int
|
||||
|
|
|
|||
39
server/tests/test_provider_model.py
Normal file
39
server/tests/test_provider_model.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# ========= 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 app.model.provider.provider import ProviderIn, VaildStatus
|
||||
|
||||
|
||||
def test_provider_in_accepts_is_valid_alias():
|
||||
provider = ProviderIn(
|
||||
provider_name="aws-bedrock-converse",
|
||||
model_type="anthropic.claude-3-5-sonnet",
|
||||
api_key="",
|
||||
endpoint_url="https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
is_valid=True,
|
||||
)
|
||||
|
||||
assert provider.is_vaild == VaildStatus.is_valid
|
||||
|
||||
|
||||
def test_provider_in_preserves_is_vaild_field():
|
||||
provider = ProviderIn(
|
||||
provider_name="aws-bedrock-converse",
|
||||
model_type="anthropic.claude-3-5-sonnet",
|
||||
api_key="",
|
||||
endpoint_url="https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
is_vaild=VaildStatus.not_valid,
|
||||
)
|
||||
|
||||
assert provider.is_vaild == VaildStatus.not_valid
|
||||
|
|
@ -130,6 +130,13 @@ const PLAN_CREDITS_BY_KEY: Record<string, number> = {
|
|||
pro: 10000,
|
||||
};
|
||||
|
||||
const VALID_PROVIDER_STATUS = 2;
|
||||
|
||||
const isProviderValid = (provider: any): boolean =>
|
||||
provider?.is_valid === true ||
|
||||
provider?.is_vaild === VALID_PROVIDER_STATUS ||
|
||||
provider?.is_vaild === 'is_valid';
|
||||
|
||||
export default function SettingModels() {
|
||||
const {
|
||||
modelType,
|
||||
|
|
@ -375,7 +382,7 @@ export default function SettingModels() {
|
|||
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,
|
||||
is_valid: isProviderValid(found),
|
||||
prefer: found.prefer ?? false,
|
||||
model_type: found.model_type ?? '',
|
||||
externalConfig: fi.externalConfig
|
||||
|
|
@ -677,7 +684,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: VALID_PROVIDER_STATUS,
|
||||
model_type: form[idx].model_type,
|
||||
};
|
||||
if (externalConfig) {
|
||||
|
|
@ -695,6 +702,9 @@ export default function SettingModels() {
|
|||
// add: refresh provider list after saving, update form and switch editable status
|
||||
const res = await proxyFetchGet('/api/v1/providers');
|
||||
const providerList = Array.isArray(res) ? res : res.items || [];
|
||||
const savedProvider = providerList.find(
|
||||
(p: any) => p.provider_name === item.id
|
||||
);
|
||||
setForm((f) =>
|
||||
f.map((fi, i) => {
|
||||
const item = items[i];
|
||||
|
|
@ -708,7 +718,7 @@ export default function SettingModels() {
|
|||
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,
|
||||
is_valid: isProviderValid(found),
|
||||
prefer: found.prefer ?? false,
|
||||
externalConfig: fi.externalConfig
|
||||
? fi.externalConfig.map((ec) => {
|
||||
|
|
@ -733,10 +743,10 @@ export default function SettingModels() {
|
|||
pendingDefaultModel.category === 'custom' &&
|
||||
pendingDefaultModel.modelId === item.id
|
||||
) {
|
||||
await handleSwitch(idx, true);
|
||||
await handleSwitch(idx, true, savedProvider?.id);
|
||||
setPendingDefaultModel(null);
|
||||
} else {
|
||||
handleSwitch(idx, true);
|
||||
await handleSwitch(idx, true, savedProvider?.id);
|
||||
}
|
||||
} finally {
|
||||
setLoading(null);
|
||||
|
|
@ -873,7 +883,7 @@ export default function SettingModels() {
|
|||
provider_name: localPlatform,
|
||||
api_key: 'not-required',
|
||||
endpoint_url: currentEndpoint, // Save base URL without specific endpoints
|
||||
is_valid: true,
|
||||
is_vaild: VALID_PROVIDER_STATUS,
|
||||
model_type: currentType,
|
||||
encrypted_config: {
|
||||
model_platform: localPlatform,
|
||||
|
|
@ -950,12 +960,19 @@ export default function SettingModels() {
|
|||
}
|
||||
}, [selectedTab, localPlatform]);
|
||||
|
||||
const handleSwitch = async (idx: number, checked: boolean) => {
|
||||
const handleSwitch = async (
|
||||
idx: number,
|
||||
checked: boolean,
|
||||
providerId?: number
|
||||
) => {
|
||||
if (!checked) {
|
||||
setActiveModelIdx(null);
|
||||
setLocalEnabled(true);
|
||||
return;
|
||||
}
|
||||
const targetProviderId =
|
||||
providerId !== undefined ? providerId : form[idx].provider_id;
|
||||
if (targetProviderId === undefined) return;
|
||||
const hasSearchKey = await checkHasSearchKey();
|
||||
if (!hasSearchKey) {
|
||||
// Show warning toast instead of blocking
|
||||
|
|
@ -968,7 +985,7 @@ export default function SettingModels() {
|
|||
}
|
||||
try {
|
||||
await proxyFetchPost('/api/v1/provider/prefer', {
|
||||
provider_id: form[idx].provider_id,
|
||||
provider_id: targetProviderId,
|
||||
});
|
||||
setModelType('custom');
|
||||
setActiveModelIdx(idx);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue