Download test/test_image_model_service.py from zlcy/study: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/zlcy/study/resolve/main/test/test_image_model_service.py
- Command line
-
hf download hf://spaces/zlcy/study/test/test_image_model_service.py
-
curl -L -o test_image_model_service.py https://huggingface.co/spaces/zlcy/study/resolve/main/test/test_image_model_service.py
10.4 kB
| from __future__ import annotations | |
| import json | |
| import tempfile | |
| import unittest | |
| from pathlib import Path | |
| from unittest import mock | |
| from fastapi import FastAPI | |
| from fastapi.testclient import TestClient | |
| from api import ai as ai_module | |
| from services.chat_model_service import resolve_chat_models | |
| from services.config import ConfigStore, _normalize_image_model_overrides | |
| from services.image_model_service import resolve_image_models | |
| def _upstream(model_id: str, **extra: object) -> dict[str, object]: | |
| return {"id": model_id, "object": "model", "owned_by": "chatgpt", **extra} | |
| def _builtin_image(model_id: str = "gpt-image-2") -> dict[str, object]: | |
| return {"id": model_id, "object": "model", "owned_by": "chatgpt2api"} | |
| def _third_party(model_id: str, capabilities: list[str] | None = None) -> dict[str, object]: | |
| return { | |
| "id": model_id, | |
| "object": "model", | |
| "owned_by": "aichimo", | |
| "group_id": "g1", | |
| "capabilities": ["txt2img", "img2img"] if capabilities is None else capabilities, | |
| } | |
| class ImageModelOverrideNormalizationTests(unittest.TestCase): | |
| def test_trims_dedupes_and_include_wins(self) -> None: | |
| normalized = _normalize_image_model_overrides({ | |
| "include": [" gpt-image-2 ", "", None, "gpt-image-2"], | |
| "exclude": ["Nano Banana 2-4k", "Nano Banana 2-4k", "gpt-image-2"], | |
| }) | |
| # include 与 exclude 冲突时以 include 为准 | |
| self.assertEqual(normalized, {"include": ["gpt-image-2"], "exclude": ["Nano Banana 2-4k"]}) | |
| def test_invalid_payloads_fall_back_to_empty(self) -> None: | |
| for value in (None, "gpt-image-2", 42, ["gpt-image-2"]): | |
| self.assertEqual(_normalize_image_model_overrides(value), {"include": [], "exclude": []}) | |
| def test_overrides_round_trip_through_config_store(self) -> None: | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| path = Path(tmp_dir) / "config.json" | |
| path.write_text(json.dumps({"auth-key": "test-auth"}), encoding="utf-8") | |
| store = ConfigStore(path) | |
| self.assertEqual(store.image_models, {"include": [], "exclude": []}) | |
| saved = store.update({"image_models": {"include": [], "exclude": ["Nano Banana 2-4k"]}}) | |
| self.assertEqual(saved["image_models"], {"include": [], "exclude": ["Nano Banana 2-4k"]}) | |
| self.assertEqual(store.get()["image_models"], {"include": [], "exclude": ["Nano Banana 2-4k"]}) | |
| # 未提及 image_models 的局部更新不应把它清掉。 | |
| store.update({"proxy": "http://proxy.example:8080"}) | |
| self.assertEqual(store.get()["image_models"], {"include": [], "exclude": ["Nano Banana 2-4k"]}) | |
| # 与对话侧互不干扰,且都能落盘。 | |
| store.update({"chat_models": {"include": ["research"], "exclude": []}}) | |
| reloaded = ConfigStore(path) | |
| self.assertEqual(reloaded.image_models, {"include": [], "exclude": ["Nano Banana 2-4k"]}) | |
| self.assertEqual(reloaded.chat_models, {"include": ["research"], "exclude": []}) | |
| class ResolveImageModelsTests(unittest.TestCase): | |
| def setUp(self) -> None: | |
| self.items = [ | |
| _upstream("auto"), | |
| _upstream("gpt-5-6"), | |
| _upstream("research"), | |
| _builtin_image("gpt-image-2"), | |
| _builtin_image("codex-gpt-image-2"), | |
| _third_party("Nano Banana 2-4k"), | |
| _third_party("gemini-3-pro-image"), | |
| _third_party("qwen3-max", capabilities=[]), | |
| # 多模态:既能聊也能生图 | |
| _third_party("omni-chat-image", capabilities=["chat", "txt2img"]), | |
| ] | |
| def resolved(self, overrides: dict[str, object] | None = None) -> dict[str, dict[str, object]]: | |
| return {item["id"]: item for item in resolve_image_models(self.items, overrides)} | |
| def test_only_image_capable_models_are_candidates(self) -> None: | |
| resolved = self.resolved() | |
| self.assertEqual( | |
| set(resolved), | |
| {"gpt-image-2", "codex-gpt-image-2", "Nano Banana 2-4k", "gemini-3-pro-image", "omni-chat-image"}, | |
| ) | |
| for model_id in ("auto", "gpt-5-6", "research", "qwen3-max"): | |
| self.assertNotIn(model_id, resolved, f"{model_id} 不能生图,不该进候选") | |
| def test_sources_are_labelled(self) -> None: | |
| resolved = self.resolved() | |
| self.assertEqual(resolved["gpt-image-2"]["image_source"], "builtin") | |
| self.assertEqual(resolved["codex-gpt-image-2"]["image_source"], "builtin") | |
| self.assertEqual(resolved["Nano Banana 2-4k"]["image_source"], "third_party") | |
| self.assertEqual(resolved["gpt-image-2"]["image_capabilities"], []) | |
| self.assertEqual(resolved["Nano Banana 2-4k"]["image_capabilities"], ["img2img", "txt2img"]) | |
| def test_all_models_default_to_enabled(self) -> None: | |
| """默认全开:装完这个功能不应该让现有生图模型凭空消失。""" | |
| for item in resolve_image_models(self.items): | |
| self.assertTrue(item["image_default_enabled"]) | |
| self.assertTrue(item["image_enabled"]) | |
| def test_exclude_hides_a_model(self) -> None: | |
| resolved = self.resolved({"include": [], "exclude": ["Nano Banana 2-4k", "gpt-image-2"]}) | |
| self.assertFalse(resolved["Nano Banana 2-4k"]["image_enabled"]) | |
| self.assertFalse(resolved["gpt-image-2"]["image_enabled"]) | |
| self.assertTrue(resolved["gemini-3-pro-image"]["image_enabled"]) | |
| def test_include_wins_over_exclude(self) -> None: | |
| resolved = self.resolved({"include": ["Nano Banana 2-4k"], "exclude": ["Nano Banana 2-4k"]}) | |
| self.assertTrue(resolved["Nano Banana 2-4k"]["image_enabled"]) | |
| def test_enabled_models_sort_first_and_builtin_before_third_party(self) -> None: | |
| resolved = resolve_image_models(self.items, {"include": [], "exclude": ["gpt-image-2"]}) | |
| keys = [(0 if item["image_enabled"] else 1, item["image_source"], item["id"]) for item in resolved] | |
| self.assertEqual(keys, sorted(keys)) | |
| self.assertEqual(resolved[-1]["id"], "gpt-image-2") | |
| def test_multimodal_model_appears_in_both_lists(self) -> None: | |
| image_ids = {item["id"] for item in resolve_image_models(self.items)} | |
| chat_flags = {item["id"]: item["chat_enabled"] for item in resolve_chat_models(self.items)} | |
| self.assertIn("omni-chat-image", image_ids) | |
| self.assertTrue(chat_flags["omni-chat-image"], "多模态模型应同时出现在对话下拉里") | |
| # 纯生图模型仍然只在生图列表、不会混进对话 | |
| self.assertIn("Nano Banana 2-4k", image_ids) | |
| self.assertFalse(chat_flags["Nano Banana 2-4k"]) | |
| def test_chat_only_third_party_model_stays_out_of_the_image_list(self) -> None: | |
| """第三方接入里的文本模型(capabilities 只有 chat)不是生图候选,也不该被当成生图模型。""" | |
| items = [_third_party("gpt-5.5", capabilities=["chat"])] | |
| self.assertEqual(resolve_image_models(items), []) | |
| chat = {item["id"]: item for item in resolve_chat_models(items)}["gpt-5.5"] | |
| self.assertFalse(chat["chat_is_image"], "文本模型不该带「生图」角标") | |
| self.assertTrue(chat["chat_default_enabled"], "文本模型应默认出现在对话下拉里") | |
| self.assertEqual(chat["chat_default_reason"], "") | |
| class ImageModelsEndpointTests(unittest.TestCase): | |
| def _get( | |
| self, | |
| payload: list[dict[str, object]], | |
| overrides: dict[str, list[str]] | None = None, | |
| third_party: list[dict[str, object]] | None = None, | |
| ): | |
| with ( | |
| mock.patch.object(ai_module, "require_identity", return_value={"role": "admin"}), | |
| mock.patch.object( | |
| ai_module.openai_v1_models, | |
| "list_models", | |
| return_value={"object": "list", "data": payload}, | |
| ), | |
| mock.patch.object( | |
| ai_module.third_party_image_service, | |
| "public_models_for_identity", | |
| return_value=third_party or [], | |
| ), | |
| mock.patch.object( | |
| type(ai_module.config), | |
| "image_models", | |
| new_callable=mock.PropertyMock, | |
| return_value=overrides or {"include": [], "exclude": []}, | |
| ), | |
| ): | |
| app = FastAPI() | |
| app.include_router(ai_module.create_router()) | |
| return TestClient(app).get("/api/image/models") | |
| def test_returns_only_image_models_with_flags(self) -> None: | |
| response = self._get([ | |
| _upstream("auto"), | |
| _upstream("gpt-5-6"), | |
| _builtin_image("gpt-image-2"), | |
| ]) | |
| self.assertEqual(response.status_code, 200) | |
| body = response.json() | |
| self.assertEqual(body["object"], "list") | |
| self.assertEqual(body["include"], []) | |
| self.assertEqual(body["exclude"], []) | |
| ids = [item["id"] for item in body["data"]] | |
| self.assertEqual(ids, ["gpt-image-2"]) | |
| self.assertTrue(body["data"][0]["image_enabled"]) | |
| self.assertEqual(body["data"][0]["image_source"], "builtin") | |
| def test_merges_third_party_image_models(self) -> None: | |
| response = self._get( | |
| [_upstream("auto"), _builtin_image("gpt-image-2")], | |
| third_party=[_third_party("Nano Banana 2-4k"), _third_party("qwen3-max", capabilities=[])], | |
| ) | |
| flags = {item["id"]: item["image_enabled"] for item in response.json()["data"]} | |
| self.assertEqual(flags, {"gpt-image-2": True, "Nano Banana 2-4k": True}) | |
| sources = {item["id"]: item["image_source"] for item in response.json()["data"]} | |
| self.assertEqual(sources["Nano Banana 2-4k"], "third_party") | |
| def test_echoes_overrides_and_applies_them(self) -> None: | |
| response = self._get( | |
| [_builtin_image("gpt-image-2")], | |
| overrides={"include": [], "exclude": ["gpt-image-2"]}, | |
| third_party=[_third_party("Nano Banana 2-4k")], | |
| ) | |
| body = response.json() | |
| self.assertEqual(body["exclude"], ["gpt-image-2"]) | |
| flags = {item["id"]: item["image_enabled"] for item in body["data"]} | |
| self.assertFalse(flags["gpt-image-2"]) | |
| self.assertTrue(flags["Nano Banana 2-4k"]) | |
| if __name__ == "__main__": | |
| unittest.main() | |