П/ВИН

Модели OpenRouter: умный роутинг к Claude Opus для кодинга

·10 мин чтения

Я перепробовал десятки конфигураций, прежде чем понял: проблема не в том, какую модель выбрать, а в том, что выбор вообще приходится делать вручную. Когда работаешь с Claude, OpenAI, Gemini и кучей дешёвых альтернатив одновременно, задача "отправить сложный кодинг-запрос к самой умной модели, а не к первой попавшейся" превращается в рутину, которая съедает время. Именно здесь модели OpenRouter меняют подход - вместо ручного переключения между провайдерами получаешь единую точку входа, где маршрутизация происходит автоматически.

Именно это мы и решали в проекте llm-router. Расскажу про три больших куска работы: перенос настроек агентов в OMP, добавление кастомного эндпоинта с умным роутингом к Opus, и выстраивание системы онбординга аккаунтов с парсером кредов прямо в UI.

Что такое llm-router и зачем он нужен

В основе проекта лежит 9Router — локальный AI-шлюз, который сидит между клиентами (Claude Code, OMP, Codex, Cursor) и множеством LLM-провайдеров. Схема простая:

Claude Code / OMP / Codex / Cursor

     http://localhost:20128/v1

             9Router

 Claude / OpenAI / Gemini / дешёвые модели

Клиент думает, что разговаривает с одним OpenAI-совместимым API. На самом деле 9Router смотрит на задачу, применяет правила роутинга и отправляет запрос туда, куда нужно. Это даёт гибкость: дорогие модели — для сложных задач, дешёвые — для рутины.

Проект живёт в .worktrees/9router-fork — это git worktree от основного репозитория, что позволяет работать с форком параллельно, не засоряя основную ветку.

Перенос агентов и настроек в OMP

Первая большая задача — инициализировать в OMP все настройки: skills, агентов, плагины и MCP-серверы, с которыми работали в Claude Code.

На первый взгляд звучит просто: скопировать конфиги из одного места в другое. На практике оказалось несколько неочевидных моментов.

Проблема 1: несовместимый frontmatter агентов. У нас было 8 активных кастомных агентов в Claude Code. OMP не подхватил их автоматически — у каждого агента был frontmatter в формате, который OMP не понимает. Пришлось конвертировать вручную, приводя к OMP-совместимому формату.

Проблема 2: Telegram skills. Выключенный Telegram всё равно обнаруживался OMP при сканировании. Пришлось явно отключить skills access и configure для Telegram, чтобы они не всплывали в интерфейсе.

Проблема 3: открытый API-ключ в агенте. В одном из агентов лежал открытый ключ — его нельзя было просто скопировать. Ключ вынесли в переменную окружения, агент переписали на чтение из env.

Проблема 4: MCP-серверы. Изначально планировали перенести и MCP-серверы, но в процессе выяснилось, что ими никто не пользуется. Приняли решение полностью исключить MCP из OMP-конфига — убрали ~/.omp/agent/mcp.json и все связанные .env файлы.

В итоге структура получилась такая:

~/.omp/
├── RULES.md          # sticky-правила проекта
├── AGENTS.md         # карта агентов и путей
├── skills/           # конвертированные skills из Claude Code
├── plugins/          # плагины
└── agents/           # 8 активных агентов (без MCP)

Важный момент — RULES.md сделали sticky-правилом: оно перецепляется к каждому ходу агента, а не тонет в начале контекста. Туда же записали, что прод крутится не из корня репозитория, а из .worktrees/9router-fork — это критично для правильной работы агентов.

Approval mode выставили в write, что даёт агентам право писать файлы без подтверждения каждого шага.

Кастомный эндпоинт с умным роутингом к Opus

Вторая задача — добавить специальный эндпоинт для задач кодинга, где основная модель Claude Opus, а не что попало.

Логика роутинга

Рассматривали три подхода:

  1. Обычный Combo — Opus первый, остальные fallback. Уже поддерживается, но это не умный роутинг: пока Opus жив, всё идёт в Opus без разбора задачи.

  2. LLM-классификатор перед каждым запросом — отдельная модель анализирует задачу и решает, куда её отправить. Понимает смысл лучше, но добавляет задержку и дополнительный запрос.

  3. Гибридный rule-based + весовой роутинг — правила на основе паттернов задачи, без дополнительных запросов к LLM.

Выбрали третий вариант с жёсткими правилами: архитектура, оркестрация, сложный debugging, неоднозначные задачи — только Opus. Если задача сомнительная — тоже Opus. Gemini и более дешёвые модели получают только явно безопасные уровни сложности.

Codex из роутинга исключили полностью.

Реализация эндпоинта

Эндпоинт добавили как отдельный маршрут в 9Router:

// routes/coding.ts
import { Router } from 'express';
import { classifyTask, OPUS_PATTERNS } from '../lib/task-classifier';
 
const router = Router();
 
router.post('/v1/coding', async (req, res) => {
  const { messages, ...rest } = req.body;
  const lastMessage = messages[messages.length - 1]?.content || '';
  
  const taskLevel = classifyTask(lastMessage);
  const targetModel = selectModel(taskLevel);
  
  // Проксируем к выбранной модели
  return proxyRequest(targetModel, { messages, ...rest }, res);
});
 
function selectModel(level: 'complex' | 'medium' | 'simple'): string {
  switch (level) {
    case 'complex':
      return 'claude-opus-4-5'; // Всегда Opus для сложного
    case 'medium':
      return 'claude-sonnet-4-5'; // Sonnet для среднего
    case 'simple':
      return process.env.CHEAP_MODEL || 'gemini-flash'; // Дешёвая для простого
  }
}
// lib/task-classifier.ts
export const OPUS_PATTERNS = [
  /архитектур/i,
  /рефактор/i,
  /debug/i,
  /оркестрац/i,
  /race condition/i,
  /concurren/i,
  /deadlock/i,
  /performance/i,
  /оптимизац/i,
];
 
export function classifyTask(text: string): 'complex' | 'medium' | 'simple' {
  // Если хоть один паттерн совпал — это сложная задача
  if (OPUS_PATTERNS.some(p => p.test(text))) {
    return 'complex';
  }
  
  // Длинные запросы тоже считаем сложными
  if (text.length > 2000) {
    return 'complex';
  }
  
  // Короткие вопросы — простые
  if (text.length < 200) {
    return 'simple';
  }
  
  return 'medium';
}

Интеграция с OMP

Чтобы OMP использовал новый эндпоинт для задач кодинга, добавили конфиг в ~/.omp/AGENTS.md:

## Coding Agent
- Endpoint: http://localhost:20128/v1/coding
- Модель: авто (Opus для сложного, Sonnet для среднего)
- Использовать для: написание кода, дебаггинг, архитектура

Теперь когда OMP запускает coding-агента, запросы идут через /v1/coding, а не через стандартный /v1/chat/completions. Роутер сам решает, какую модель использовать.

Система онбординга аккаунтов

Третья задача выросла из реальной боли: в системе видео-пула Google-аккаунты теряли свои креды. Конкретно — у шестого аккаунта не было сохранённых данных, потому что их скидывали в чат, но не записывали в реестр.

Правило про креды

Первым делом добавили sticky-правило в .omp/RULES.md:

## Правило: учётные данные аккаунтов
 
При добавлении любого Google-аккаунта в систему — в ту же сессию
записать в реестр:
- email
- password  
- TOTP-seed (2FA секрет)
- recoveryEmail
- recoveryEmailPassword
- backupCodes (резервные коды)
 
Если кредов нет на руках — спросить у пользователя.
Никогда не оставлять поля пустыми молча.

Мастер онбординга

Дальше спроектировали UI-мастер для добавления новых аккаунтов. Ключевая идея: пользователь кидает в окно чата дамп с кредами от продавца, сервер парсит его и заполняет карточку аккаунта.

Это решает главную проблему — креды больше не теряются в истории чата.

Архитектура мастера:

Шаг 1: Базовые данные
  └── Пользователь вставляет дамп от продавца
  └── Сервер парсит email, пароль, TOTP, резервные коды
  └── Показывает распознанное для подтверждения

Шаг 2: Автоматические действия (делает агент)
  └── Создание профиля браузера
  └── Первый вход через noVNC
  └── Подключение к Flow/Gemini/Dola/Antigravity

Шаг 3: Ручные действия (делает пользователь)
  └── Первый вход в Google (требует живого клика)
  └── Подтверждение consent-экрана

Шаг 4: Проверка владения
  └── Смена пароля
  └── Обновление 2FA
  └── Выход со всех устройств
  └── Статус: «только у нас» / «не только у нас»

Парсер кредов на сервере:

// lib/creds-parser.ts
interface ParsedCreds {
  email?: string;
  password?: string;
  totpSeed?: string;
  recoveryEmail?: string;
  recoveryEmailPassword?: string;
  backupCodes?: string[];
}
 
export function parseCreds(raw: string): ParsedCreds {
  const result: ParsedCreds = {};
  
  // Email
  const emailMatch = raw.match(/([\w.+-]+@[\w-]+\.[\w.]+)/);
  if (emailMatch) result.email = emailMatch[1];
  
  // Пароль — ищем после типичных подписей
  const passMatch = raw.match(
    /(?:пароль|password|pass)[:\s]+([^\s\n]+)/i
  );
  if (passMatch) result.password = passMatch[1];
  
  // TOTP seed — обычно 32 символа base32
  const totpMatch = raw.match(/([A-Z2-7]{32,})/);
  if (totpMatch) result.totpSeed = totpMatch[1];
  
  // Резервные коды — обычно 8-значные числа
  const backupMatches = raw.match(/\b\d{8}\b/g);
  if (backupMatches?.length >= 3) {
    result.backupCodes = backupMatches;
  }
  
  return result;
}

Статус владения аккаунтом

Отдельно реализовали чек-лист владения — в UI видно, «только у нас» аккаунт или нет:

interface OwnershipStatus {
  passwordChanged: boolean;    // Сменили пароль
  twoFaUpdated: boolean;       // Обновили 2FA
  loggedOutAll: boolean;       // Вышли со всех устройств
  recoveryEmailOwned: boolean; // Резервная почта наша
}
 
function isFullyOwned(status: OwnershipStatus): boolean {
  return Object.values(status).every(Boolean);
}

В карточке аккаунта это отображается как зелёный/красный индикатор с расшифровкой, каких шагов не хватает.

Работа с concurrency и блокерами

В процессе разработки наткнулись на несколько технических блокеров, которые проверяли запуском тестов.

Один из них — проблема конкурентной записи в реестр аккаунтов. Когда два процесса одновременно пишут в один файл реестра, данные могут перезаписать друг друга. Проверяли это на /tmp-копии с двумя параллельными writer-процессами:

# Тест конкурентной записи
for i in 1 2; do
  node scripts/write-registry.js --account acc$i &
done
wait
 
# Проверяем целостность
node scripts/verify-registry.js

Решение — файловая блокировка через proper-lockfile с retry-логикой:

import { lock, unlock } from 'proper-lockfile';
 
async function writeToRegistry(accountId: string, data: object) {
  const registryPath = '/path/to/registry.json';
  
  await lock(registryPath, { retries: 5 });
  try {
    const registry = JSON.parse(await fs.readFile(registryPath, 'utf8'));
    registry[accountId] = { ...registry[accountId], ...data };
    await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
  } finally {
    await unlock(registryPath);
  }
}

Файл реестра с кредами получил права 0600 — читает только владелец процесса.

Результаты

После всей этой работы получили:

  • 8 агентов перенесены в OMP с правильным frontmatter
  • Кастомный эндпоинт /v1/coding с умным роутингом: сложные задачи → Opus, средние → Sonnet, простые → дешёвая модель
  • Sticky-правила в .omp/RULES.md — агент всегда знает контекст проекта
  • UI-мастер онбординга аккаунтов с парсером кредов
  • Чек-лист владения — видно, «только у нас» аккаунт или нет
  • Конкурентная запись в реестр защищена файловой блокировкой
  • Реестр с кредами имеет права 0600

Выводы и уроки

Главный урок этого проекта — простые задачи не нужно усложнять. Перенос агентов из Claude Code в OMP — это по сути конвертация конфигов. Не нужны подзадачи, ревью-циклы и worktree-изоляция для каждого шага. Иногда лучший процесс — это его отсутствие.

Второй урок — данные должны жить в системе, а не в чате. История переписки — плохое место для хранения паролей и TOTP-секретов. Как только мы выстроили процесс «вставил дамп → сервер распарсил → записал в реестр», проблема потери кредов исчезла сама собой. Sticky-правила в .omp/RULES.md помогают агенту не забывать об этом требовании.

Третий урок — умный роутинг не обязательно означает LLM-классификатор. Добавлять отдельный запрос к модели ради классификации задачи — это overhead. Rule-based подход с паттернами работает достаточно хорошо для большинства случаев и не добавляет задержку. Если задача содержит слова «архитектура», «рефактор», «race condition» — она идёт в Opus. Просто и надёжно.

Четвёртый урок — MCP не нужен, если им не пользуются. Мы потратили время на планирование переноса MCP-серверов, а потом выяснилось, что ими никто не пользуется. Лучше сначала спросить «а это вообще нужно?», чем реализовывать ради галочки.

Пятый урок — безопасность кредов — это не опция. Открытый API-ключ в агенте, права 0777 на реестр, секреты в истории чата — всё это реальные риски. Права 0600 на файл с кредами, вынос секретов в env, парсинг без вывода значений в лог — базовые вещи, которые нужно делать сразу, а не потом.

Проект продолжает развиваться: следующий шаг — добавить метрики роутинга, чтобы видеть, сколько запросов уходит в каждую модель и какова средняя стоимость запроса через /v1/coding по сравнению со стандартным эндпоинтом.

Полезные ссылки

  • Claude API документация — официальная документация Anthropic
  • OpenAI API Reference — совместимый API, который эмулирует 9Router
  • proper-lockfile — файловые блокировки для Node.js
  • Git Worktrees — работа с несколькими рабочими деревьями
  • Gemini API — документация Google Gemini, один из провайдеров в роутере
Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.