@@ -64,10 +64,10 @@ def __init__(
6464 key_id : str ,
6565 secret : str ,
6666 base_url : str ,
67+ project : str ,
6768 # Параметры совместимые с OpenAI SDK
6869 api_key : Optional [str ] = None , # Игнорируется
6970 organization : Optional [str ] = None ,
70- project : Optional [str ] = None ,
7171 timeout : Union [float , None ] = None ,
7272 max_retries : int = 2 ,
7373 default_headers : Optional [Dict [str , str ]] = None ,
@@ -84,9 +84,11 @@ def __init__(
8484 # Сохраняем Cloud.ru credentials
8585 self .key_id = key_id
8686 self .secret = secret
87+ self .project = project
8788
8889 # Инициализируем token manager
8990 self .token_manager = EvolutionTokenManager (key_id , secret )
91+ self ._need_token_refresh : bool = False
9092
9193 # Получаем первоначальный токен
9294 initial_token = self .token_manager .get_valid_token ()
@@ -105,66 +107,41 @@ def __init__(
105107 ** kwargs ,
106108 )
107109
108- # Переопределяем _client для автоматического обновления токенов
109- self ._patch_client ()
110-
111- def _patch_client (self ) -> None : # type: ignore[reportUnknownMemberType]
112- """Патчим client для автоматического обновления токенов"""
113- # В новых версиях используется 'request'
114- if hasattr (self ._client , "request" ): # type: ignore[reportUnknownMemberType,reportUnknownArgumentType]
115- original_request = self ._client .request # type: ignore[reportUnknownMemberType,reportUnknownVariableType]
116- method_name = "request"
117- else :
118- logger .warning ("Не удалось найти метод request в HTTP клиенте" )
119- return
120-
121- def patched_request (* args : Any , ** kwargs : Any ) -> Any : # type: ignore[reportUnknownMemberType,reportUnknownArgumentType,reportUnknownVariableType,reportUnknownReturnType]
122- # Обновляем токен перед каждым запросом
123- current_token = self .token_manager .get_valid_token ()
124- self .api_key = current_token or "" # type: ignore[reportUnknownMemberType]
125- self ._update_auth_headers (current_token or "" )
126-
127- try :
128- return original_request (* args , ** kwargs )
129- except Exception as e :
130- # Если ошибка авторизации, принудительно обновляем токен
131- if self ._is_auth_error (e ):
132- logger .warning (
133- "Ошибка авторизации, принудительно обновляем токен"
134- )
135- self .token_manager .invalidate_token ()
136- new_token = self .token_manager .get_valid_token ()
137- self .api_key = new_token or "" # type: ignore[reportUnknownMemberType]
138- # Повторяем запрос с новым токеном
139- self ._update_auth_headers (new_token or "" )
140- return original_request (* args , ** kwargs )
141- else :
142- raise
143-
144- # Устанавливаем патченый метод
145- setattr (self ._client , method_name , patched_request ) # type: ignore[reportUnknownMemberType,reportUnknownArgumentType]
146-
147- def _update_auth_headers (self , token : str ) -> None :
148- """Обновляет заголовки авторизации"""
149- auth_header = f"Bearer { token } "
150- if hasattr (self ._client , "_auth_headers" ):
151- self ._client ._auth_headers ["Authorization" ] = auth_header # type: ignore[reportAttributeAccessIssue]
152- elif hasattr (self ._client , "default_headers" ):
153- self ._client .default_headers ["Authorization" ] = auth_header # type: ignore[reportAttributeAccessIssue]
154-
155- def _is_auth_error (self , error : Exception ) -> bool :
156- """Проверяет, является ли ошибка связанной с авторизацией"""
157- error_str = str (error ).lower ()
158- return any (
159- keyword in error_str
160- for keyword in [
161- "unauthorized" ,
162- "401" ,
163- "authentication" ,
164- "forbidden" ,
165- "403" ,
166- ]
167- )
110+ @override
111+ def _should_retry (self , response : Any ) -> bool : # type: ignore[reportUnknownMemberType]
112+ """Определяет, нужно ли повторять запрос для данного ответа.
113+
114+ При получении 401 или 403 инициирует обновление токена и позволяет выполнить повтор.
115+
116+ :param response: Ответ httpx.Response от сервера.
117+ :return: True если нужно сделать retry, иначе — результат родительского метода.
118+ """
119+ if response .status_code in (401 , 403 ):
120+ self ._need_token_refresh = True
121+ return True
122+ return super ()._should_retry (response ) # type: ignore[reportUnknownMemberType]
123+
124+ @override
125+ def _prepare_request (self , request : Any ) -> None : # type: ignore[reportUnknownMemberType]
126+ """Мутирует объект запроса перед отправкой.
127+
128+ При необходимости обновляет токен авторизации
129+ и всегда добавляет заголовок x-project-id с текущим проектом.
130+
131+ :param request: Объект httpx.Request, готовящийся к отправке.
132+ """
133+ if self ._need_token_refresh or not self .is_token_valid :
134+ token = self .refresh_token ()
135+ self .api_key = token
136+ request .headers ["Authorization" ] = f"Bearer { token } "
137+ self ._need_token_refresh = False
138+ request .headers ["x-project-id" ] = self .project
139+
140+
141+ @property
142+ def is_token_valid (self ) -> bool :
143+ """Возвращает статус валидности токена."""
144+ return self .token_manager .is_token_valid ()
168145
169146 @property
170147 def current_token (self ) -> Optional [str ]:
@@ -231,10 +208,10 @@ def __init__(
231208 key_id : str ,
232209 secret : str ,
233210 base_url : str ,
211+ project : str ,
234212 # Параметры совместимые с AsyncOpenAI
235213 api_key : Optional [str ] = None ,
236214 organization : Optional [str ] = None ,
237- project : Optional [str ] = None ,
238215 timeout : Union [float , None ] = None ,
239216 max_retries : int = 2 ,
240217 default_headers : Optional [Dict [str , str ]] = None ,
@@ -250,10 +227,11 @@ def __init__(
250227 # Сохраняем Cloud.ru credentials
251228 self .key_id = key_id
252229 self .secret = secret
230+ self .project = project
253231
254232 # Инициализируем token manager
255233 self .token_manager = EvolutionTokenManager (key_id , secret )
256-
234+ self . _need_token_refresh : bool = False
257235 # Получаем первоначальный токен
258236 initial_token = self .token_manager .get_valid_token ()
259237
@@ -271,66 +249,41 @@ def __init__(
271249 ** kwargs ,
272250 )
273251
274- # Патчим async client
275- self ._patch_async_client ()
276-
277- def _patch_async_client (self ) -> None :
278- """Патчим async client для автоматического обновления токенов"""
279- # В новых версиях используется 'request'
280- if hasattr (self ._client , "request" ): # type: ignore[reportUnknownMemberType,reportUnknownArgumentType]
281- original_request = self ._client .request # type: ignore[reportUnknownMemberType,reportUnknownVariableType]
282- method_name = "request"
283- else :
284- logger .warning (
285- "Не удалось найти метод request в async HTTP клиенте"
286- )
287- return
288-
289- async def patched_request (* args : Any , ** kwargs : Any ) -> Any : # type: ignore[reportUnknownMemberType,reportUnknownArgumentType,reportUnknownVariableType,reportUnknownReturnType]
290- # Обновляем токен перед каждым запросом
291- current_token = self .token_manager .get_valid_token ()
292- self .api_key = current_token or "" # type: ignore[reportUnknownMemberType,reportUnknownVariableType]
293- self ._update_auth_headers (current_token or "" )
294-
295- try :
296- return await original_request (* args , ** kwargs )
297- except Exception as e :
298- if self ._is_auth_error (e ):
299- logger .warning (
300- "Ошибка авторизации, принудительно обновляем токен"
301- )
302- self .token_manager .invalidate_token ()
303- new_token = self .token_manager .get_valid_token ()
304- self .api_key = new_token or "" # type: ignore[reportUnknownMemberType,reportUnknownVariableType]
305- self ._update_auth_headers (new_token or "" )
306- return await original_request (* args , ** kwargs )
307- else :
308- raise
309-
310- # Устанавливаем патченый метод
311- setattr (self ._client , method_name , patched_request ) # type: ignore[reportUnknownMemberType,reportUnknownArgumentType]
312-
313- def _update_auth_headers (self , token : str ) -> None :
314- """Обновляет заголовки авторизации"""
315- auth_header = f"Bearer { token } "
316- if hasattr (self ._client , "_auth_headers" ):
317- self ._client ._auth_headers ["Authorization" ] = auth_header # type: ignore[reportAttributeAccessIssue]
318- elif hasattr (self ._client , "default_headers" ):
319- self ._client .default_headers ["Authorization" ] = auth_header # type: ignore[reportAttributeAccessIssue]
320-
321- def _is_auth_error (self , error : Exception ) -> bool :
322- """Проверяет, является ли ошибка связанной с авторизацией"""
323- error_str = str (error ).lower ()
324- return any (
325- keyword in error_str
326- for keyword in [
327- "unauthorized" ,
328- "401" ,
329- "authentication" ,
330- "forbidden" ,
331- "403" ,
332- ]
333- )
252+ @override
253+ def _should_retry (self , response : Any ) -> bool : # type: ignore[reportUnknownMemberType]
254+ """Определяет, нужно ли повторять запрос для данного ответа.
255+
256+ При получении 401 или 403 инициирует обновление токена и позволяет выполнить повтор.
257+
258+ :param response: Ответ httpx.Response от сервера.
259+ :return: True если нужно сделать retry, иначе — результат родительского метода.
260+ """
261+ if response .status_code in (401 , 403 ):
262+ self ._need_token_refresh = True
263+ return True
264+ return super ()._should_retry (response ) # type: ignore[reportUnknownMemberType]
265+
266+ @override
267+ async def _prepare_request (self , request : Any ) -> None : # type: ignore[reportUnknownMemberType]
268+ """Мутирует объект запроса перед отправкой.
269+
270+ При необходимости обновляет токен авторизации
271+ и всегда добавляет заголовок x-project-id с текущим проектом.
272+
273+ :param request: Объект httpx.Request, готовящийся к отправке.
274+ """
275+ if self ._need_token_refresh or not self .is_token_valid :
276+ token = self .refresh_token ()
277+ self .api_key = token
278+ request .headers ["Authorization" ] = f"Bearer { token } "
279+ self ._need_token_refresh = False
280+ request .headers ["x-project-id" ] = self .project
281+
282+
283+ @property
284+ def is_token_valid (self ) -> bool :
285+ """Возвращает статус валидности токена."""
286+ return self .token_manager .is_token_valid ()
334287
335288 @property
336289 def current_token (self ) -> Optional [str ]:
0 commit comments