Найдено при проверке Grok на живых данных владельца. Вызов падал с
AttributeError: 'str' object has no attribute 'get' — grok_adapter.py:103.
Поле error провайдеры отдают то объектом {"message": ...}, то строкой.
Код безусловно звал .get у результата, и на строковой форме ОБРАБОТЧИК
ОШИБОК ПАДАЛ САМ: сбой происходил ровно там, где обрабатывался другой
сбой, маршрутизация обрывалась вместо перехода к резерву, а настоящая
причина терялась.
После правки причина видна: Grok API Error (403): The OAuth2 access token
could not be validated. То есть у Grok просто протух токен, а выглядело
как поломка кода.
Та же конструкция стояла ещё в пяти адаптерах: claude, codex, opencode,
deepseek, local. Разбор вынесен в base_adapter.extract_api_error_message,
все шесть переведены на него.
Тест проверяет обе формы ответа и отдельно следит, чтобы копии хрупкой
конструкции не вернулись.
Тесты: 411 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
deepseek_adapter.py had never been importable: it referenced ProviderAdapter
and ProfileConfig, neither of which exists in src/, and profile.extra, which
RouterProfileConfig does not define. Rewritten against BaseProviderAdapter with
the three missing abstract methods and registered as "deepseek".
Adds tests/test_import_invariants.py, which walks every shipped module and
fails on broken internal references while skipping absent optional GUI extras.
It also asserts that test modules importing customtkinter call
pytest.importorskip, since a missing guard aborts collection of the whole
session and takes the release gate with it.
Adds a headless CI job that uninstalls customtkinter and runs the suite, so
that invariant is enforced rather than remembered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>