
У меня MacBook Pro 16 2019 года: шестиядерный Intel Core i7, 16 ГБ оперативной памяти и Radeon Pro 5300M с 4 ГБ VRAM. На нём уже стояли Ollama, gemma3:4b и gemma2:2b.
Идея казалась очевидной: зачем тратить облачный Codex на разбор короткого traceback, сводку лога или commit message? Пусть Codex отдаёт такую работу локальной модели, получает готовый результат и занимается только сложными задачами.
Я сделал MCP-сервер, подключил его к Codex, прогнал одинаковые задачи и посмотрел точный расход токенов. Интеграция заработала. Экономия — нет.
На одной простой задаче маршрут через локальную модель использовал примерно в два раза больше облачного input, чем прямой ответ Codex.
Ниже — не инструкция «как запустить нейронку за пять минут», а разбор эксперимента: что я собрал, где ошибся в первоначальной гипотезе и для каких задач маленькая локальная модель всё-таки пригодилась.
Что именно я хотел получить
Не второй чат рядом с Codex, а локального исполнителя внутри рабочего процесса:
Я ставлю задачу Codex.
Codex понимает, что задача простая.
Передаёт её Gemma через локальный инструмент.
Проверяет ответ.
Не расходует дорогой облачный inference на саму рутину.
Для делегирования я выбрал MCP. Codex поддерживает локальные STDIO MCP-серверы: процесс запускается на компьютере, читает JSON-RPC из stdin и возвращает результат через stdout. Конфигурация хранится в ~/.codex/config.toml.
Сервер получился маленьким и без внешних Python-зависимостей. У него один инструмент — local_llm.
{ "prompt": "Что означает эта ошибка?", "context": "SMTPAuthenticationError: 535 authentication failed", "mode": "classify", "max_tokens": 160, "temperature": 0 }
В ответ приходят не только слова модели, но и измерения:
{ "answer": "Incorrect username or password for the email server", "model": "gemma3:4b", "wall_time_ms": 6098, "prompt_tokens": 71, "output_tokens": 44, "tokens_per_second": 9.52 }
Промпты и исходный код я в журнал не пишу. Сохраняются только SHA-256 промпта, длина контекста, модель, время и число токенов. Иначе «локальный и приватный помощник» незаметно превратился бы в ещё одно хранилище исходников.
Первый запуск не состоялся
Codex увидел сервер и попытался вызвать инструмент, но codex exec ответил:
MCP tool call requires approval, but approval policy is never
Поведение правильное. Неинтерактивный процесс не может показать диалог подтверждения, поэтому новый инструмент нельзя молча выполнить.
Для конкретного read-only инструмента я явно разрешил вызовы:
[mcp_servers.ollama-worker] command = "codex-ollama-worker" default_tools_approval_mode = "approve" [mcp_servers.ollama-worker.tools.local_llm] approval_mode = "approve" output_token_limit = 1200
После этого цепочка заработала: Codex выбрал local_llm, Gemma ответила, а Codex прочитал структурированный результат и вернул итог.
На что способны 2B и 4B
Я составил восемь небольших задач. Они основаны на ошибках, с которыми столкнулся в обычном проекте:
несовпадение hostname в TLS-сертификате;
SMTP
535;извлечение портов из Docker Compose;
path traversal при загрузке файла;
старая запись, навсегда блокирующая новую;
защита рассылки от повторной отправки;
добавление
UNIQUEв таблицу с существующими дублями;commit message.
Одинаковые промпты запускались с temperature=0.

Модель | Время восьми задач | Медиана | Средняя скорость | Сгенерировано |
|---|---|---|---|---|
| 147,1 с | 9,4 с | 9,40 ток/с | 1202 токена |
| 109,1 с | 13,8 с | 14,19 ток/с | 1328 токенов |
Обе модели работали на CPU. Несмотря на наличие дискретной Radeon с 4 ГБ VRAM, Ollama показывала 100% CPU.
На простых преобразованиях всё было нормально. Обе модели извлекли внешний и внутренний порт, назвали Redis зависимостью, поняли SMTP 535 и написали приемлемый commit message.
Но уже на коротком security review началось интересное.
Я дал gemma3:4b этот фрагмент:
target = Path('/srv/uploads') / upload.filename with target.open('wb') as output: output.write(await upload.read())
Модель упомянула directory traversal, но главным исправлением предложила заменить бинарный режим wb на текстовый w.
То есть опасное имя файла осталось опасным, а загрузка бинарных файлов дополнительно сломалась.
gemma2:2b выступила чуть лучше: она правильно указала на пользовательский upload.filename, но затем посоветовала использовать Path.joinpath() или os.path.join(). Эти функции соединяют пути, но не обезвреживают ../../etc/passwd.
На логике записей обе модели также промахнулись:
existing = await db.execute( select(Enrollment).where(Enrollment.user_id == user.id) ) if existing.scalars().first(): raise HTTPException(400, 'Уже записан')
Настоящая проблема: запрос учитывает вообще любую старую запись. После первого занятия пользователь может навсегда потерять возможность записываться дальше.
4B-модель вместо этого предложила перевести русское сообщение об ошибке на английский. 2B заметила, что проверяется любая запись, но объяснила это настолько путано, что готовое исправление из ответа получить нельзя.
Вывод по качеству получился простой: маленькие модели подходят для механической обработки текста, но не для окончательного ревью безопасности и предметной логики.
Где исчезла экономия
После работающего вызова я повторил одну и ту же простую задачу тремя способами: напрямую через Ollama, напрямую через Codex и через связку Codex → MCP → Ollama → Codex.
Задача везде была одна: объяснить SMTP 535 authentication failed и назвать первую проверку.

Маршрут | Время | Облачный input | Облачный output | Локальные токены |
|---|---|---|---|---|
Ollama напрямую | 6,1 с | 0 | 0 | 71 + 44 |
Codex напрямую | 8,8 с | 14 309 | 49 | 0 |
Codex → MCP → Ollama → Codex | 21,4 с | 29 301 | 384 | 71 + 44 |
Почему через MCP получилось дороже?
Потому что локальная модель не заменяет облачный проход. Сначала Codex должен прочитать запрос и решить, какой инструмент вызвать. После выполнения инструмента Codex снова получает контекст вместе с результатом и формирует финальный ответ.
Для короткой задачи получаются два облачных прохода вместо одного. Локальная модель выполняет полезную работу посередине, но сама её работа почти ничего не вычитает из контекста Codex.
Важно: числа взяты из событий turn.completed, а не из процентов в интерфейсе. Проценты слишком грубые, к тому же параллельно шла разработка самого сервера.
А можно запустить весь Codex локально?
В CLI есть OSS-режим:
codex exec --oss --local-provider ollama -m gemma3:4b
Я попробовал и его. Сначала запуск упал, потому что Codex запросил thinking, а gemma3:4b его не поддерживает. Я отключил reasoning, но получил следующую ошибку:
registry.ollama.ai/library/gemma3:4b does not support tools
Даже когда в пользовательском запросе написано «не вызывай инструменты», Codex работает как агент и передаёт модели доступные tool-схемы. Установленная Gemma 3 4B не поддерживает такой режим.
То есть на этой машине получился работающий локальный MCP-исполнитель, но не полностью локальный Codex-агент.
Что в итоге имеет смысл
Моя исходная схема была поставлена задом наперёд.
Если облачный Codex уже начал ход, вызов маленькой локальной модели не обязан экономить лимит. Для мелкой задачи он может потратить больше из-за второго облачного прохода.
Экономия появляется только тогда, когда простая задача полностью заканчивается локально — до обращения к Codex.
Рабочее разделение для моего железа выглядит так:
Локально:
извлечь факты из лога;
сжать длинный вывод команды;
преобразовать JSON или таблицу;
написать черновик commit message;
классифицировать знакомую ошибку.
В Codex:
менять код;
искать причину инцидента в нескольких слоях;
работать с миграциями;
проверять безопасность;
принимать архитектурные решения;
перепроверять сомнительный локальный ответ.
MCP-сервер я всё равно оставил. Как локальный инструмент он работает, возвращает измеримые результаты и может быть полезен для приватной предварительной обработки. Просто называть его «экономией лимита» после этого эксперимента было бы неправдой.
Следующий шаг — сделать local-first оболочку: сначала задача попадает в Ollama, а в Codex уходит только по явной эскалации. Именно такую схему уже можно честно проверять на экономию недельного лимита.

