fix: add stream close check, recover regression test assertions, and secure credentials type check
This commit is contained in:
@@ -1397,9 +1397,11 @@ def _sync_upstream_models_generator(wid: int, db: Session):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 1. 先安全修复 base_url (不依赖同步模型结果)
|
# 1. 先安全修复 base_url (不依赖同步模型结果)
|
||||||
# 在 remote_acc.get("credentials") 基础上过滤掉敏感字段,保留其他非敏感 credentials (例如 compact_model_mapping)
|
raw_creds = remote_acc.get("credentials")
|
||||||
|
if not isinstance(raw_creds, dict):
|
||||||
|
raw_creds = {}
|
||||||
current_creds = {
|
current_creds = {
|
||||||
k: v for k, v in (remote_acc.get("credentials") or {}).items()
|
k: v for k, v in raw_creds.items()
|
||||||
if k not in SENSITIVE_CREDENTIAL_KEYS
|
if k not in SENSITIVE_CREDENTIAL_KEYS
|
||||||
}
|
}
|
||||||
current_creds["base_url"] = upstream_base_url
|
current_creds["base_url"] = upstream_base_url
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch):
|
|||||||
db_session.add(key2)
|
db_session.add(key2)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
|
updates_recorded = []
|
||||||
|
|
||||||
class FakeClient:
|
class FakeClient:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
@@ -79,13 +81,23 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch):
|
|||||||
pass
|
pass
|
||||||
def list_accounts(self):
|
def list_accounts(self):
|
||||||
return [
|
return [
|
||||||
{"id": "1", "name": "Account 1", "credentials": {"base_url": "old"}},
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Account 1",
|
||||||
|
"credentials": {
|
||||||
|
"base_url": "old",
|
||||||
|
"access_token": "secret_access_token", # Sensitive credential
|
||||||
|
"api_key": "some_api_key", # Sensitive credential
|
||||||
|
"openai_capabilities": "some_capabilities", # Non-sensitive field
|
||||||
|
"compact_model_mapping": "some_mapping", # Non-sensitive field
|
||||||
|
}
|
||||||
|
},
|
||||||
{"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}}
|
{"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}}
|
||||||
]
|
]
|
||||||
def extract_id(self, val):
|
def extract_id(self, val):
|
||||||
return val["id"]
|
return val["id"]
|
||||||
def update_account(self, aid, data):
|
def update_account(self, aid, data):
|
||||||
pass
|
updates_recorded.append((aid, data))
|
||||||
def sync_account_upstream_models(self, aid):
|
def sync_account_upstream_models(self, aid):
|
||||||
if aid == "1":
|
if aid == "1":
|
||||||
return ["gpt-4", "gpt-3.5"]
|
return ["gpt-4", "gpt-3.5"]
|
||||||
@@ -118,6 +130,29 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch):
|
|||||||
|
|
||||||
assert items_map["2"]["status"] == "failed"
|
assert items_map["2"]["status"] == "failed"
|
||||||
assert items_map["2"]["model_count"] == 0
|
assert items_map["2"]["model_count"] == 0
|
||||||
|
|
||||||
|
# Verify update payload regression safety
|
||||||
|
# Account 1 should be updated twice (first to fix base_url, second to set model_mapping)
|
||||||
|
acc1_updates = [up for up in updates_recorded if up[0] == "1"]
|
||||||
|
assert len(acc1_updates) == 2
|
||||||
|
|
||||||
|
# 1. First update to fix base_url
|
||||||
|
payload1 = acc1_updates[0][1]["credentials"]
|
||||||
|
assert payload1["base_url"] == "http://up1.api"
|
||||||
|
assert "access_token" not in payload1
|
||||||
|
assert "api_key" not in payload1
|
||||||
|
assert payload1["openai_capabilities"] == "some_capabilities"
|
||||||
|
assert payload1["compact_model_mapping"] == "some_mapping"
|
||||||
|
|
||||||
|
# 2. Second update to write back model_mapping
|
||||||
|
payload2 = acc1_updates[1][1]["credentials"]
|
||||||
|
assert payload2["base_url"] == "http://up1.api"
|
||||||
|
assert "access_token" not in payload2
|
||||||
|
assert "api_key" not in payload2
|
||||||
|
assert payload2["openai_capabilities"] == "some_capabilities"
|
||||||
|
assert payload2["compact_model_mapping"] == "some_mapping"
|
||||||
|
assert payload2["model_mapping"] == {"gpt-3.5": "gpt-3.5", "gpt-4": "gpt-4"}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
@@ -163,6 +198,8 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch):
|
|||||||
db_session.add(key2)
|
db_session.add(key2)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
|
updates_recorded = []
|
||||||
|
|
||||||
class FakeClient:
|
class FakeClient:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
@@ -172,13 +209,23 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch):
|
|||||||
pass
|
pass
|
||||||
def list_accounts(self):
|
def list_accounts(self):
|
||||||
return [
|
return [
|
||||||
{"id": "1", "name": "Account 1", "credentials": {"base_url": "old"}},
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Account 1",
|
||||||
|
"credentials": {
|
||||||
|
"base_url": "old",
|
||||||
|
"access_token": "secret_access_token", # Sensitive credential
|
||||||
|
"api_key": "some_api_key", # Sensitive credential
|
||||||
|
"openai_capabilities": "some_capabilities", # Non-sensitive field
|
||||||
|
"compact_model_mapping": "some_mapping", # Non-sensitive field
|
||||||
|
}
|
||||||
|
},
|
||||||
{"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}}
|
{"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}}
|
||||||
]
|
]
|
||||||
def extract_id(self, val):
|
def extract_id(self, val):
|
||||||
return val["id"]
|
return val["id"]
|
||||||
def update_account(self, aid, data):
|
def update_account(self, aid, data):
|
||||||
pass
|
updates_recorded.append((aid, data))
|
||||||
def sync_account_upstream_models(self, aid):
|
def sync_account_upstream_models(self, aid):
|
||||||
if aid == "1":
|
if aid == "1":
|
||||||
return ["gpt-4", "gpt-3.5"]
|
return ["gpt-4", "gpt-3.5"]
|
||||||
@@ -223,6 +270,26 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch):
|
|||||||
assert parsed_events[3]["event"] == "complete"
|
assert parsed_events[3]["event"] == "complete"
|
||||||
assert parsed_events[3]["data"]["success"] is False
|
assert parsed_events[3]["data"]["success"] is False
|
||||||
assert len(parsed_events[3]["data"]["items"]) == 2
|
assert len(parsed_events[3]["data"]["items"]) == 2
|
||||||
|
|
||||||
|
# Verify update payload regression safety for stream
|
||||||
|
acc1_updates = [up for up in updates_recorded if up[0] == "1"]
|
||||||
|
assert len(acc1_updates) == 2
|
||||||
|
|
||||||
|
# First update to fix base_url
|
||||||
|
payload1 = acc1_updates[0][1]["credentials"]
|
||||||
|
assert payload1["base_url"] == "http://up1.api"
|
||||||
|
assert "access_token" not in payload1
|
||||||
|
assert "api_key" not in payload1
|
||||||
|
assert payload1["openai_capabilities"] == "some_capabilities"
|
||||||
|
|
||||||
|
# Second update to write back model_mapping
|
||||||
|
payload2 = acc1_updates[1][1]["credentials"]
|
||||||
|
assert payload2["base_url"] == "http://up1.api"
|
||||||
|
assert "access_token" not in payload2
|
||||||
|
assert "api_key" not in payload2
|
||||||
|
assert payload2["openai_capabilities"] == "some_capabilities"
|
||||||
|
assert payload2["model_mapping"] == {"gpt-3.5": "gpt-3.5", "gpt-4": "gpt-4"}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|||||||
@@ -1986,6 +1986,7 @@ async function triggerSyncUpstreamModels() {
|
|||||||
syncModelsDialog.value = true
|
syncModelsDialog.value = true
|
||||||
|
|
||||||
let hasProcessedStart = false
|
let hasProcessedStart = false
|
||||||
|
let gotComplete = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@@ -2040,6 +2041,7 @@ async function triggerSyncUpstreamModels() {
|
|||||||
syncModelsResults.value.push(data)
|
syncModelsResults.value.push(data)
|
||||||
} else if (event === 'complete') {
|
} else if (event === 'complete') {
|
||||||
syncModelsMessage.value = data.message
|
syncModelsMessage.value = data.message
|
||||||
|
gotComplete = true
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
ElMessage.success('同步完成')
|
ElMessage.success('同步完成')
|
||||||
} else {
|
} else {
|
||||||
@@ -2061,6 +2063,7 @@ async function triggerSyncUpstreamModels() {
|
|||||||
syncModelsResults.value.push(data)
|
syncModelsResults.value.push(data)
|
||||||
} else if (event === 'complete') {
|
} else if (event === 'complete') {
|
||||||
syncModelsMessage.value = data.message
|
syncModelsMessage.value = data.message
|
||||||
|
gotComplete = true
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
ElMessage.success('同步完成')
|
ElMessage.success('同步完成')
|
||||||
} else {
|
} else {
|
||||||
@@ -2070,6 +2073,10 @@ async function triggerSyncUpstreamModels() {
|
|||||||
throw new Error(data.message || '流式同步出错')
|
throw new Error(data.message || '流式同步出错')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!gotComplete) {
|
||||||
|
throw new Error('同步流异常中断,未获取到完整执行报告')
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e.message || '同步上游模型失败')
|
ElMessage.error(e.message || '同步上游模型失败')
|
||||||
if (!hasProcessedStart || syncModelsResults.value.length === 0) {
|
if (!hasProcessedStart || syncModelsResults.value.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user