fix: distinguish image connection and generation checks
This commit is contained in:
@@ -249,6 +249,7 @@ export function SettingsCenter({ open, onClose, onReadinessChange }: SettingsCen
|
|||||||
});
|
});
|
||||||
const result = await response.json() as TestResult;
|
const result = await response.json() as TestResult;
|
||||||
setTestResults((current) => ({ ...current, [target]: result }));
|
setTestResults((current) => ({ ...current, [target]: result }));
|
||||||
|
await loadSettings();
|
||||||
setNotice({ tone: result.ok ? "success" : "error", text: result.message });
|
setNotice({ tone: result.ok ? "success" : "error", text: result.message });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotice({ tone: "error", text: getErrorMessage(error) });
|
setNotice({ tone: "error", text: getErrorMessage(error) });
|
||||||
@@ -568,7 +569,7 @@ function ModelPoolPanel({ draft, tests, busy, onChange, onTest, onTrial }: Model
|
|||||||
<div className="model-editor-heading">
|
<div className="model-editor-heading">
|
||||||
<div>
|
<div>
|
||||||
<h3>{pool.some((item) => item.id === editor.id) ? "编辑模型" : "添加模型"}</h3>
|
<h3>{pool.some((item) => item.id === editor.id) ? "编辑模型" : "添加模型"}</h3>
|
||||||
<p>测试会核对当前账号的模型列表,并准确指出哪个模型 ID 不可用。</p>
|
<p>“验证连接”检查地址、密钥和模型列表;部分媒体模型不会出现在列表中,最终以“试生成”的端到端结果为准。</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="icon-button small" type="button" onClick={() => setEditor(null)} aria-label="关闭模型编辑器">
|
<button className="icon-button small" type="button" onClick={() => setEditor(null)} aria-label="关闭模型编辑器">
|
||||||
<XIcon size={16} />
|
<XIcon size={16} />
|
||||||
@@ -815,7 +816,7 @@ function ModelGroup({ title, description, category, models, tests, busy, onAdd,
|
|||||||
<div className="model-row-actions">
|
<div className="model-row-actions">
|
||||||
<button className="button button-secondary compact" type="button" onClick={() => onTest(model.id)} disabled={Boolean(busy) || !model.enabled}>
|
<button className="button button-secondary compact" type="button" onClick={() => onTest(model.id)} disabled={Boolean(busy) || !model.enabled}>
|
||||||
{busy === target ? <ArrowClockwiseIcon className="spin" size={14} /> : <PlugIcon size={14} />}
|
{busy === target ? <ArrowClockwiseIcon className="spin" size={14} /> : <PlugIcon size={14} />}
|
||||||
验证模型
|
验证连接
|
||||||
</button>
|
</button>
|
||||||
{model.capabilities.includes("image_generation") ? (
|
{model.capabilities.includes("image_generation") ? (
|
||||||
<button className="button button-secondary compact" type="button" onClick={() => onTrial(model.id)} disabled={Boolean(busy) || !model.enabled} title="会消耗一次模型调用额度">
|
<button className="button button-secondary compact" type="button" onClick={() => onTrial(model.id)} disabled={Boolean(busy) || !model.enabled} title="会消耗一次模型调用额度">
|
||||||
|
|||||||
+2
-2
@@ -39,8 +39,8 @@
|
|||||||
|
|
||||||
每个模型提供两种测试:
|
每个模型提供两种测试:
|
||||||
|
|
||||||
- “验证模型”只访问模型列表并检查协议和路径,不生图、不扣费,用于定位模型 ID、凭证或配置错误。
|
- “验证连接”只检查地址、API Key、模型列表、协议和路径,不生图、不扣费。部分聚合平台不会在 `/v1/models` 中公开媒体模型 ID,此时不能据此判定模型不可用。
|
||||||
- “试生成”会在用户确认后真实生成一张低成本测试图,会消耗一次调用额度,用来证明创建任务、轮询状态和取得结果的完整链路可用。
|
- “试生成”会在用户确认后真实生成一张低成本测试图并消耗一次调用额度。其成功结果是生图模型端到端可用性的最终依据,并会自动将该模型标记为已通过测试。
|
||||||
|
|
||||||
总调度模型必须由用户从大语言模型池中指定。空间理解和生图各自支持两种方式:
|
总调度模型必须由用户从大语言模型池中指定。空间理解和生图各自支持两种方式:
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,21 @@ async def test_model_generation(
|
|||||||
store: EncryptedSettingsStore = Depends(get_runtime_store),
|
store: EncryptedSettingsStore = Depends(get_runtime_store),
|
||||||
) -> RuntimeSettingsTestResult:
|
) -> RuntimeSettingsTestResult:
|
||||||
try:
|
try:
|
||||||
return await test_image_generation(store.merged_values(), model_id)
|
values = store.merged_values()
|
||||||
|
result = await test_image_generation(values, model_id)
|
||||||
|
store.record_test(result, values)
|
||||||
|
definitive_result = RuntimeSettingsTestResult(
|
||||||
|
target=f"model:{model_id}",
|
||||||
|
ok=result.ok,
|
||||||
|
message=(
|
||||||
|
result.message
|
||||||
|
if not result.ok
|
||||||
|
else "已通过一次真实生图端到端验证;模型、鉴权、请求参数、任务轮询和结果读取均可用。"
|
||||||
|
),
|
||||||
|
details=result.details,
|
||||||
|
)
|
||||||
|
store.record_test(definitive_result, values)
|
||||||
|
return result
|
||||||
except (RuntimeError, ValueError) as exc:
|
except (RuntimeError, ValueError) as exc:
|
||||||
return RuntimeSettingsTestResult(
|
return RuntimeSettingsTestResult(
|
||||||
target=f"generation:{model_id}",
|
target=f"generation:{model_id}",
|
||||||
|
|||||||
@@ -342,7 +342,9 @@ class EncryptedSettingsStore:
|
|||||||
document["tests"] = {
|
document["tests"] = {
|
||||||
target: result
|
target: result
|
||||||
for target, result in document["tests"].items()
|
for target, result in document["tests"].items()
|
||||||
if not target.startswith("model:") or target.removeprefix("model:") in current_ids
|
if not (
|
||||||
|
target.startswith("model:") or target.startswith("generation:")
|
||||||
|
) or target.split(":", 1)[1] in current_ids
|
||||||
}
|
}
|
||||||
self.save_document(document)
|
self.save_document(document)
|
||||||
|
|
||||||
@@ -525,8 +527,8 @@ class EncryptedSettingsStore:
|
|||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
current: dict[str, dict[str, Any]] = {}
|
current: dict[str, dict[str, Any]] = {}
|
||||||
for target, result in tests.items():
|
for target, result in tests.items():
|
||||||
if target.startswith("model:"):
|
if target.startswith("model:") or target.startswith("generation:"):
|
||||||
model_id = target.removeprefix("model:")
|
model_id = target.split(":", 1)[1]
|
||||||
if not any(item.get("id") == model_id for item in values.get("model_pool", [])):
|
if not any(item.get("id") == model_id for item in values.get("model_pool", [])):
|
||||||
continue
|
continue
|
||||||
item = dict(result)
|
item = dict(result)
|
||||||
@@ -542,8 +544,8 @@ class EncryptedSettingsStore:
|
|||||||
"database_url": self.bootstrap.database_url,
|
"database_url": self.bootstrap.database_url,
|
||||||
"redis_url": self.bootstrap.redis_url,
|
"redis_url": self.bootstrap.redis_url,
|
||||||
}
|
}
|
||||||
elif target.startswith("model:"):
|
elif target.startswith("model:") or target.startswith("generation:"):
|
||||||
model_id = target.removeprefix("model:")
|
model_id = target.split(":", 1)[1]
|
||||||
relevant = next(
|
relevant = next(
|
||||||
(item for item in values.get("model_pool", []) if item.get("id") == model_id),
|
(item for item in values.get("model_pool", []) if item.get("id") == model_id),
|
||||||
{"id": model_id, "missing": True},
|
{"id": model_id, "missing": True},
|
||||||
@@ -742,7 +744,34 @@ async def test_model_pool_item(values: dict[str, Any], model_id: str) -> Runtime
|
|||||||
for candidate in rows
|
for candidate in rows
|
||||||
if isinstance(candidate, dict) and candidate.get("id")
|
if isinstance(candidate, dict) and candidate.get("id")
|
||||||
}
|
}
|
||||||
|
protocol_error = _validate_image_protocol(item)
|
||||||
|
if protocol_error:
|
||||||
|
return RuntimeSettingsTestResult(
|
||||||
|
target=target,
|
||||||
|
ok=False,
|
||||||
|
message=f"模型“{name}”的生图协议配置不正确:{protocol_error}",
|
||||||
|
details={"model_id": item["model_id"], "model_count": len(listed_ids)},
|
||||||
|
)
|
||||||
|
is_aigc_media = (
|
||||||
|
"image_generation" in item.get("capabilities", [])
|
||||||
|
and item.get("image_protocol") == "aigc_media"
|
||||||
|
)
|
||||||
if not listed_ids:
|
if not listed_ids:
|
||||||
|
if is_aigc_media:
|
||||||
|
return RuntimeSettingsTestResult(
|
||||||
|
target=target,
|
||||||
|
ok=True,
|
||||||
|
message=(
|
||||||
|
f"模型“{name}”的 API Key 与模型列表接口连接正常;平台未在 /v1/models 中公开媒体模型 ID"
|
||||||
|
f"“{item['model_id']}”,这里只完成连接与鉴权验证。最终可用性以“试生成”为准。"
|
||||||
|
),
|
||||||
|
details={
|
||||||
|
"model_id": item["model_id"],
|
||||||
|
"model_count": 0,
|
||||||
|
"id_advertised": False,
|
||||||
|
"verification": "connectivity_only",
|
||||||
|
},
|
||||||
|
)
|
||||||
return RuntimeSettingsTestResult(
|
return RuntimeSettingsTestResult(
|
||||||
target=target,
|
target=target,
|
||||||
ok=False,
|
ok=False,
|
||||||
@@ -750,20 +779,27 @@ async def test_model_pool_item(values: dict[str, Any], model_id: str) -> Runtime
|
|||||||
details={"model_id": item["model_id"], "model_count": 0},
|
details={"model_id": item["model_id"], "model_count": 0},
|
||||||
)
|
)
|
||||||
if item["model_id"] not in listed_ids:
|
if item["model_id"] not in listed_ids:
|
||||||
|
if is_aigc_media:
|
||||||
|
return RuntimeSettingsTestResult(
|
||||||
|
target=target,
|
||||||
|
ok=True,
|
||||||
|
message=(
|
||||||
|
f"模型“{name}”的 API Key 与模型列表接口连接正常;平台返回了 {len(listed_ids)} 个模型,"
|
||||||
|
f"但未公开媒体模型 ID“{item['model_id']}”。这不是生图失败,最终可用性以“试生成”为准。"
|
||||||
|
),
|
||||||
|
details={
|
||||||
|
"model_id": item["model_id"],
|
||||||
|
"model_count": len(listed_ids),
|
||||||
|
"id_advertised": False,
|
||||||
|
"verification": "connectivity_only",
|
||||||
|
},
|
||||||
|
)
|
||||||
return RuntimeSettingsTestResult(
|
return RuntimeSettingsTestResult(
|
||||||
target=target,
|
target=target,
|
||||||
ok=False,
|
ok=False,
|
||||||
message=f"模型“{name}”测试失败:账号模型列表中未找到 ID“{item['model_id']}”。",
|
message=f"模型“{name}”测试失败:账号模型列表中未找到 ID“{item['model_id']}”。",
|
||||||
details={"model_id": item["model_id"], "model_count": len(listed_ids)},
|
details={"model_id": item["model_id"], "model_count": len(listed_ids)},
|
||||||
)
|
)
|
||||||
protocol_error = _validate_image_protocol(item)
|
|
||||||
if protocol_error:
|
|
||||||
return RuntimeSettingsTestResult(
|
|
||||||
target=target,
|
|
||||||
ok=False,
|
|
||||||
message=f"模型“{name}”的 ID 可用,但生图协议配置不正确:{protocol_error}",
|
|
||||||
details={"model_id": item["model_id"], "model_count": len(listed_ids)},
|
|
||||||
)
|
|
||||||
return RuntimeSettingsTestResult(
|
return RuntimeSettingsTestResult(
|
||||||
target=target,
|
target=target,
|
||||||
ok=True,
|
ok=True,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.integrations.openai_compatible import OpenAICompatibleGateway
|
from app.integrations.openai_compatible import OpenAICompatibleGateway
|
||||||
|
from app.runtime_settings import RuntimeSettingsTestResult, test_model_pool_item as run_model_pool_test
|
||||||
|
|
||||||
|
|
||||||
def image_values(profile: str, model_id: str) -> dict:
|
def image_values(profile: str, model_id: str) -> dict:
|
||||||
@@ -110,3 +111,67 @@ async def test_aigc_media_polls_task_until_result_url(monkeypatch) -> None:
|
|||||||
|
|
||||||
assert result["state"] == "success"
|
assert result["state"] == "success"
|
||||||
assert result["data"] == [{"url": "https://cdn.example.com/final.png"}]
|
assert result["data"] == [{"url": "https://cdn.example.com/final.png"}]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_aigc_media_connection_accepts_model_list_omission(monkeypatch) -> None:
|
||||||
|
class FakeResponse:
|
||||||
|
def raise_for_status(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return {"data": [{"id": "language-model-only"}]}
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get(self, *args, **kwargs) -> FakeResponse:
|
||||||
|
return FakeResponse()
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.runtime_settings.httpx.AsyncClient", lambda **kwargs: FakeClient())
|
||||||
|
|
||||||
|
result = await run_model_pool_test(
|
||||||
|
image_values("gpt_image_2", "gpt-image-2"),
|
||||||
|
"image",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.ok is True
|
||||||
|
assert result.details["id_advertised"] is False
|
||||||
|
assert result.details["verification"] == "connectivity_only"
|
||||||
|
assert "最终可用性以“试生成”为准" in result.message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generation_endpoint_records_definitive_model_result(monkeypatch) -> None:
|
||||||
|
from app.api.settings import test_model_generation
|
||||||
|
|
||||||
|
class FakeStore:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.recorded: list[RuntimeSettingsTestResult] = []
|
||||||
|
|
||||||
|
def merged_values(self) -> dict:
|
||||||
|
return image_values("gpt_image_2", "gpt-image-2")
|
||||||
|
|
||||||
|
def record_test(self, result: RuntimeSettingsTestResult, values: dict) -> None:
|
||||||
|
self.recorded.append(result)
|
||||||
|
|
||||||
|
async def fake_generation(values: dict, model_id: str) -> RuntimeSettingsTestResult:
|
||||||
|
return RuntimeSettingsTestResult(
|
||||||
|
target=f"generation:{model_id}",
|
||||||
|
ok=True,
|
||||||
|
message="端到端可用",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.api.settings.test_image_generation", fake_generation)
|
||||||
|
store = FakeStore()
|
||||||
|
|
||||||
|
result = await test_model_generation("image", store=store)
|
||||||
|
|
||||||
|
assert result.ok is True
|
||||||
|
assert [item.target for item in store.recorded] == ["generation:image", "model:image"]
|
||||||
|
assert store.recorded[1].ok is True
|
||||||
|
assert "真实生图端到端验证" in store.recorded[1].message
|
||||||
|
|||||||
Reference in New Issue
Block a user