Глава 4: Разговор с Claude — API-слой
Глава 3 показала, где живёт состояние и как два слоя обмениваются данными. Теперь посмотрим, что происходит, когда это состояние начинает использоваться: системе нужно разговаривать с языковой моделью. Всё в Claude Code — bootstrap-последовательность, система состояния, framework разрешений — существует ради этого момента.
Этот слой обрабатывает больше режимов отказа, чем любая другая часть системы. Он должен маршрутизировать через четыре облачных провайдера через единый прозрачный интерфейс. Он должен собирать системные prompts с поблочной точностью и понимать, как работает prompt cache сервера, потому что одна неверно поставленная секция может пробить кэш на 50 000+ токенов. Он должен стримить ответы с активным обнаружением отказов, потому что TCP-соединения умирают молча. И он должен сохранять инварианты, стабильные на уровне сессии, чтобы изменения feature flags в середине разговора не создавали невидимых провалов производительности.
Проследим один API-вызов от начала до конца.
Фабрика клиента для нескольких провайдеров
Функция getAnthropicClient() — единственная фабрика для всей model communication. Она возвращает клиент Anthropic SDK, настроенный под того провайдера, который использует деплой:
Диспетчеризация полностью основана на переменных окружения и вычисляется в фиксированном порядке приоритета. Все четыре provider-specific SDK-класса приводятся к Anthropic через as unknown as Anthropic. Комментарий в исходнике предельно честен: «мы всегда врали про тип возвращаемого значения». Это намеренное стирание типов означает, что каждый consumer видит единый интерфейс. Остальная кодовая база никогда не ветвится по провайдеру.
Каждый provider SDK импортируется динамически — AnthropicBedrock, AnthropicFoundry, AnthropicVertex — это тяжёлые модули со своими деревьями зависимостей. Динамический импорт гарантирует, что неиспользуемые провайдеры никогда не загрузятся.
Выбор провайдера определяется при старте и сохраняется в bootstrap STATE. Цикл запроса никогда не проверяет, какой провайдер активен. Переход с Direct API на Bedrock — это изменение конфигурации, а не изменение кода.
Обёртка buildFetch
Каждый исходящий fetch оборачивается, чтобы добавить заголовок x-client-request-id — UUID, генерируемый на каждый запрос. Когда запрос истекает по таймауту, сервер никогда не присваивает request ID ответу. Без клиентского ID команда API не может связать таймаут с серверными логами. Этот header закрывает разрыв. Он отправляется только на first-party конечные точки Anthropic — сторонние провайдеры могут отвергнуть неизвестные заголовки.
Построение системного prompt
Системный prompt — самый чувствительный к кэшу артефакт во всей системе. Claude API предоставляет server-side prompt caching: идентичные префиксы prompt между запросами могут кэшироваться, экономя и задержку, и стоимость. Разговор на 200K токенов может содержать 50–70K токенов, идентичных предыдущему ходу. Пробой этого кэша заставляет сервер заново обработать всё это.
Динамическая граница
Prompt строится как массив строковых секций с критической разделительной линией:
Всё до границы одинаково для всех сессий, пользователей и организаций — оно получает самый высокий уровень server-side caching. Всё после неё содержит пользовательский контент и опускается до кэширования на уровне сессии.
Соглашение по именованию секций намеренно громкое. Добавление новой секции требует выбрать между systemPromptSection (безопасная, кэшируемая) и DANGEROUS_uncachedSystemPromptSection (ломает кэш, требует строку с обоснованием). Параметр _reason не используется во время выполнения, но служит обязательной документацией — каждая секция, ломающая кэш, несёт своё оправдание в исходном коде.
Проблема 2^N
Комментарий в prompts.ts объясняет, почему условные секции должны идти после границы:
Каждое условие здесь — это runtime-бит, который иначе умножал бы варианты Blake2b prefix hash (2^N).
Каждое булево условие до границы удваивает число уникальных глобальных записей кэша. Три условия создают 8 вариантов; пять — 32. Статические секции намеренно безусловны. Compile-time feature flags (разрешённые bundler’ом) допустимы до границы. Runtime checks (это Haiku? есть ли у пользователя auto mode?) должны идти после.
Это тот тип ограничения, который незаметен, пока вы его не нарушите. Инженер, добавивший секцию, зависящую от user setting, перед границей, может молча фрагментировать глобальный кэш и удвоить fleet-стоимость обработки prompts.
Стриминг
Сырой SSE вместо SDK-абстракций
Реализация streaming использует сырой Stream<BetaRawMessageStreamEvent> вместо более высокого уровня BetaMessageStream из SDK. Причина: BetaMessageStream вызывает partialParse() на каждом событии input_json_delta. Для tool calls с большими JSON-входами (редактирование файлов с сотнями строк) это заново парсит растущую JSON-строку с нуля на каждом chunk — поведение O(n^2). Claude Code сам накапливает входные данные инструментов, так что частичный парсинг — чистая трата.
Watchdog простоя
TCP-соединения могут умереть без уведомления. Сервер может упасть, load balancer может молча оборвать соединение, корпоративный proxy может истечь по таймауту. Request timeout SDK покрывает только начальный fetch — как только приходит HTTP 200, таймаут считается удовлетворённым. Если body стрима перестаёт приходить, это никто не ловит.
Watchdog: setTimeout, который сбрасывается на каждый полученный chunk. Если в течение 90 секунд не приходит ни одного chunk, поток abort’ится, и система откатывается к non-streaming retry. На отметке 45 секунд срабатывает warning. Когда watchdog срабатывает, он логирует событие с client request ID для корреляции.
Резервный non-streaming путь
Когда streaming падает в середине ответа (сетевой сбой, зависание, обрезка), система откатывается к синхронному вызову messages.create(). Это покрывает сбои proxy, когда proxy возвращает HTTP 200 с non-SSE body или обрезает SSE stream на полпути.
Резервный путь можно отключить, когда активировано streaming tool execution, потому что откат повторно выполнит весь запрос и потенциально запустит инструменты дважды.
Система prompt cache
Три уровня
Prompt caching работает на трёх уровнях:
Эфемерный кэш (по умолчанию): кэширование на уровне сессии с TTL, определяемым сервером (~5 минут). Это получают все пользователи.
1-часовой TTL: пользователи, имеющие право, получают расширенный кэш. Право определяется статусом подписки и защёлкивается в bootstrap state — sticky latch promptCache1hEligible из главы 3 гарантирует, что переключение overage в середине сессии не изменит TTL.
Global scope: записи системного prompt-кэша разделяются между сессиями и организациями. Статические части prompt идентичны для всех пользователей Claude Code, так что одна закэшированная копия обслуживает всех. Global scope отключается, когда присутствуют MCP tools, потому что определения MCP-инструментов зависят от пользователя и фрагментировали бы кэш на миллионы уникальных префиксов.
Липкие latch’и в действии
Пять липких latch’ей из главы 3 оцениваются здесь, во время построения запроса. Каждый latch стартует как null и, будучи установлен в true, остаётся true на всю сессию. Комментарий над блоком latch’ей точен: «Sticky-on latches for dynamic beta headers. Каждый header, однажды впервые отправленный, продолжает отправляться до конца сессии, чтобы переключения в середине сессии не меняли server-side cache key и не пробивали ~50–70K токенов.»
См. главу 3, раздел 3.1 для полного объяснения паттерна latch’ей, пяти конкретных latch’ей и того, почему решение «всегда отправлять все заголовки» — не то, что нужно.
Генератор queryModel
Функция queryModel() — это async generator (~700 строк), который оркестрирует весь жизненный цикл API-вызова. Он выдаёт объекты StreamEvent, AssistantMessage и SystemAPIErrorMessage.
Сбор запроса следует аккуратно упорядоченной последовательности:
- Проверка kill switch — предохранитель для самого дорогого tier модели
- Сбор beta headers — model-specific, с применёнными sticky latch’ами
- Построение схемы инструментов — параллельно через
Promise.all(), отложенные инструменты исключаются до момента обнаружения - Нормализация сообщений — исправление несоответствий orphaned
tool_use/tool_result, удаление лишних media, удаление устаревших блоков - Построение блоков системного prompt — разделение по динамической границе, назначение cache scopes
- Стриминг с retry-обёрткой — обрабатывает 529 (overloaded), fallback модели, downgrade thinking, refresh OAuth
Лимит выходных токенов
По умолчанию лимит выхода — 8 000 токенов, а не типичные 32K или 64K. Производственные данные показали, что p99 вывода — 4 911 токенов: стандартные лимиты переизбыточны в 8–16 раз. Когда ответ достигает лимита (<1% запросов), он получает один чистый retry на 64K. Это экономит значительную сумму на масштабе fleet.
Обработка ошибок и retry
Функция withRetry() сама является async generator и выдаёт события SystemAPIErrorMessage, чтобы UI мог показывать статус повторных попыток. Стратегии retry:
- 529 (overloaded): подождать и повторить, при необходимости понижая fast mode
- Model fallback: основная модель падает, пробуем запасную (например, Opus → Sonnet)
- Thinking downgrade: переполнение окна контекста приводит к сокращению thinking budget
- OAuth 401: обновить token и повторить один раз
Паттерн генератора означает, что прогресс повторов («Сервер перегружен, повтор через 5 секунд…») появляется как естественная часть потока событий, а не как уведомление по side channel.
Примените это
Рассматривайте prompt caching как архитектурное ограничение, а не как feature toggle. Большинство LLM-приложений просто «включают» кэширование. Claude Code рассматривает его как ограничение дизайна, которое влияет на порядок prompt, мемоизацию секций, защёлкивание заголовков и управление конфигурацией. Разница между хорошо структурированным prompt (cache hit на 50K токенов) и плохо структурированным (полная переработка на каждом ходу) — это самый большой рычаг стоимости в системе.
Используйте соглашение DANGEROUS для дорогих escape hatch’ей. Когда в кодовой базе есть инвариант, который легко нарушить случайно, громкий префикс у escape hatch делает три вещи: делает нарушения заметными на code review, заставляет документировать их (обязательный параметр reason) и создаёт психологическое трение в пользу безопасного дефолта. Это обобщается не только на кэширование, но и на любую операцию с невидимой стоимостью.
Стройте streaming с watchdog, а не только с timeout. Request timeout SDK удовлетворяется на HTTP 200, но тело ответа может перестать приходить в любой момент. setTimeout, сбрасываемый на каждый chunk, ловит это. Non-streaming fallback покрывает режимы отказа proxy (HTTP 200 без SSE body, обрезка посреди стрима), которые в корпоративных средах встречаются чаще, чем кажется.
Делайте стратегии retry основанными на yield, а не на исключениях. Если обёртка retry — async generator, который выдаёт status events, вызывающая сторона показывает прогресс повторов как естественную часть потока событий. Паттерн fallback модели (Opus падает, пробуем Sonnet) особенно полезен для production-устойчивости.
Отделяйте fast path от полного пайплайна. Не каждый API-вызов требует поиска инструментов, интеграции advisor, thinking budgets и streaming-инфраструктуры. Функция queryHaiku() в Claude Code даёт упрощённый путь для внутренних операций (compaction, classification), который пропускает все agentic concerns. Отдельная функция с упрощённым интерфейсом не даёт сложности просачиваться туда, где она не нужна.
Взгляд вперёд
API-слой лежит в основании всего, что будет дальше. Глава 5 покажет, как цикл запроса использует потоковый ответ, чтобы управлять выполнением инструментов — включая то, как инструменты начинают выполняться до того, как модель завершит ответ. Глава 6 объяснит, как система compaction сохраняет эффективность кэша, когда разговоры приближаются к пределу контекста. Глава 7 покажет, как каждой агентской нити соответствует свой массив сообщений и цепочка запросов.
Все эти системы наследуют ограничения, установленные здесь: стабильность кэша как архитектурный инвариант, прозрачность провайдера через фабрику клиента и стабильность конфигурации на уровне сессии через систему latch’ей. API-слой не просто отправляет запросы — он определяет правила, по которым работает каждая другая система.