@@ -839,6 +839,79 @@ def import_credential(self, raw): # pragma: no cover - 未使用
839839 assert [m ["id" ] for m in response .json ()["data" ]] == ["glm-5.2" ]
840840
841841
842+ def test_models_failure_is_negatively_cached (settings ):
843+ """拉取失败也刷 TTL 时间戳:TTL 内不再重试(有缓存用缓存,无缓存跳过)。"""
844+
845+ class Flaky :
846+ id = "trae"
847+ attempts = 0
848+
849+ async def list_models (self , credential_data ):
850+ type (self ).attempts += 1
851+ raise RuntimeError ("upstream down" )
852+
853+ def import_credential (self , raw ): # pragma: no cover - 未使用
854+ return raw
855+
856+ app = build_app (settings , providers = {"trae" : Flaky ()})
857+ app .state .credentials .add (provider = "trae" , credential_data = {"accessToken" : "a" })
858+ services = app .state .services
859+ key = app .state .api_keys .create ("root" )["api_key" ]
860+ auth = {"Authorization" : f"Bearer { key } " }
861+ with TestClient (app ) as c :
862+ first = c .get ("/v1/models" , headers = auth )
863+ second = c .get ("/v1/models" , headers = auth )
864+ assert first .status_code == 200 and first .json ()["data" ] == []
865+ assert second .json ()["data" ] == []
866+ assert Flaky .attempts == 1 # 第二次命中负缓存,没重试
867+ assert services .model_list_fetched_at ["trae" ] is not None
868+
869+ # 清掉尝试时间戳(模拟 TTL 过期)→ 重新尝试
870+ services .model_list_fetched_at .clear ()
871+ with TestClient (app ) as c :
872+ c .get ("/v1/models" , headers = auth )
873+ assert Flaky .attempts == 2
874+
875+
876+ def test_models_failure_keeps_cache_for_ttl (settings ):
877+ """失败后 TTL 内用缓存兜底且不重试;TTL 过期才再次尝试。"""
878+
879+ class Flaky :
880+ id = "trae"
881+ fail = False
882+ attempts = 0
883+
884+ async def list_models (self , credential_data ):
885+ type (self ).attempts += 1
886+ if self .fail :
887+ raise RuntimeError ("upstream down" )
888+ return [Model (id = "glm-5.2" )]
889+
890+ def import_credential (self , raw ): # pragma: no cover - 未使用
891+ return raw
892+
893+ provider = Flaky ()
894+ app = build_app (settings , providers = {"trae" : provider })
895+ app .state .credentials .add (provider = "trae" , credential_data = {"accessToken" : "a" })
896+ services = app .state .services
897+ key = app .state .api_keys .create ("root" )["api_key" ]
898+ auth = {"Authorization" : f"Bearer { key } " }
899+ with TestClient (app ) as c :
900+ first = c .get ("/v1/models" , headers = auth )
901+ assert [m ["id" ] for m in first .json ()["data" ]] == ["glm-5.2" ]
902+ assert Flaky .attempts == 1
903+
904+ provider .fail = True
905+ services .model_list_fetched_at .clear () # 模拟 TTL 过期 → 触发重试并失败
906+ second = c .get ("/v1/models" , headers = auth )
907+ assert [m ["id" ] for m in second .json ()["data" ]] == ["glm-5.2" ]
908+ assert Flaky .attempts == 2
909+
910+ third = c .get ("/v1/models" , headers = auth ) # 失败后的 TTL 内:不再重试
911+ assert [m ["id" ] for m in third .json ()["data" ]] == ["glm-5.2" ]
912+ assert Flaky .attempts == 2
913+
914+
842915@pytest .mark .asyncio
843916async def test_disconnect_with_bytes_but_no_usage_still_recorded ():
844917 """已吐字节但上游未给 usage 时,断开也要记账(ttfb 分支)。"""
0 commit comments