This commit is contained in:
Tao Sun 2026-06-15 21:24:24 -04:00 committed by GitHub
commit 0c84435e20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 108 additions and 24 deletions

View file

@ -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)

View file

@ -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

View 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

View file

@ -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);