Перейти к содержимому
OmniRoute source

open-sse Architecture (Русский)

Зачем нужен отдельный пакет рабочего пространства?

Заголовок раздела «Зачем нужен отдельный пакет рабочего пространства?»

open-sse/ является автономным рабочим пространством в монорепозитории OmniRoute по нескольким причинам:

  1. Повторное использование — open-sse публикуется в npm как @omniroute/open-sse, поэтому другие проекты могут использовать его независимо
  2. Чёткие границы — потоковое ядро отделено от специфичного для OmniRoute слоя пользовательского интерфейса и базы данных
  3. Производительность — ядро не зависит от Next.js, что обеспечивает более быстрый холодный запуск в средах CLI и serverless
  4. Управление версиями — open-sse может выпускать релизы по собственному графику
package.json
"workspaces": ["open-sse"]

open-sse/
├── index.ts # Публичная точка входа
├── types.d.ts # Экспорт публичных типов
├── package.json # @omniroute/open-sse
├── config/ # Конфигурации провайдеров, константы, реестры
├── executors/ # HTTP-исполнители для отдельных провайдеров (67 + base.ts/index.ts)
├── handlers/ # Обработчики запросов (chatCore, responses и т. д.)
├── lib/ # Внутренние утилиты
├── mcp-server/ # Сервер Model Context Protocol
├── services/ # Около 298 сервисных модулей
├── transformer/ # Преобразователь формата Responses API
├── translator/ # Преобразование форматов (OpenAI ↔ Claude ↔ Gemini)
└── utils/ # Общие утилиты (журналирование, ошибки, потоки и т. д.)

| Каталог | Файлы | Назначение | | executors/ | 167 | HTTP-исполнители для отдельных провайдеров (унифицированы через фабрику DefaultExecutor) | | handlers/ | 157 | Точки входа запросов (chatCore, responses, embeddings) | | services/ | ~536 | Маршрутизация, кэширование, ограничение частоты запросов, обновление и т. д. | | translator/ | 56 | Преобразование форматов (OpenAI ↔ Claude ↔ Gemini) | | mcp-server/ | 44 | Инструменты и транспорты MCP | | utils/ | ~108 | Сквозные утилиты (журналирование, ошибки, потоки) | | config/ | ~339 | Конфигурации провайдеров, константы, реестры |


Каждый запрос к LLM проходит через пятиэтапный конвейер:

┌──────────────┐
HTTP-запрос │ 1. МАРШРУТ │ разрешение комбинации, выбор модели
(маршрут Next.js) └──────┬───────┘
│
▼
┌──────────────┐
│2. ПРЕОБРАЗОВ.│ преобразование форматов (OpenAI ↔ Claude ↔ Gemini)
└──────┬───────┘
│
▼
┌──────────────┐
│ 3. ВЫПОЛНЕНИЕ│ исполнитель провайдера, HTTP, повтор, предохранитель
└──────┬───────┘
│
▼
┌──────────────┐
│ 4. ПОТОК │ преобразование SSE, обратное давление
└──────┬───────┘
│
▼
┌──────────────┐
│ 5. ЗАПИСЬ │ учёт использования, журнал вызовов, классификация ошибок
└──────┬───────┘
│
▼
HTTP-ответ (SSE или JSON)

Точка входа: handleComboChat() в services/combo.ts

Сопоставляет запрос с конкретным кортежем (provider, model, account, credentials):

  • Находит комбинацию по идентификатору (или создаёт виртуальную комбинацию для моделей auto/*)
  • Применяет стратегию маршрутизации (приоритетную, взвешенную, циклическую и т. д.)
  • Отфильтровывает неработоспособных провайдеров (предохранитель)
  • Выбирает следующую подходящую цель

Для моделей auto/* на этом этапе также выполняется следующее:

  • Запускается алгоритм 16-факторной оценки (services/autoCombo/)
  • Выбирается пара provider+model на основе работоспособности, стоимости, задержки и других факторов

Если исходный формат (например, OpenAI) отличается от целевого формата (например, Claude), запрос преобразуется:

  • Системная подсказка → системное сообщение
  • Определения инструментов → формат инструментов конкретного провайдера
  • Параметры рассуждения/мышления → соответствующие параметры конкретного провайдера
  • Нормализация ролей сообщений (developer → system для провайдеров, отличных от OpenAI)

translator/index.ts экспортирует:

translateRequest(body, sourceFormat, targetFormat): TranslatedRequest
needsTranslation(source, target): boolean

Точка входа: getExecutor(providerId).execute(request, options)

Все провайдеры используют DefaultExecutor (executors/default.ts) через резервный механизм фабрики getExecutor(). Исполнитель:

  • Формирует URL вышестоящего сервиса (buildUrl())
  • Добавляет заголовки, специфичные для провайдера (buildHeaders())
  • Преобразует тело запроса (transformRequest())
  • Отправляет HTTP-запрос с повторными попытками и экспоненциальной задержкой
  • При необходимости обновляет данные аутентификации (для провайдеров OAuth)

Все исполнители расширяют BaseExecutor (executors/base.ts, 1170 строк кода), который предоставляет:

  • Общую логику повторных попыток
  • Интеграцию с прокси
  • Интеграцию с предохранителем
  • Хуки для записи данных об использовании

Для потоковых ответов исполнитель возвращает ReadableStream. Обработчик:

  • Пропускает данные через преобразователь SSE (createSSETransformStreamWithLogger)
  • Использует периодические сигналы для обнаружения разорванных соединений
  • Корректно обрабатывает отключение клиента (pipeWithDisconnect)
  • Преобразует SSE → JSON для клиентов, не поддерживающих потоковую передачу

Для непотоковых ответов исполнитель возвращает разобранный JSON-объект, который передаётся дальше без изменений.

После ответа (успешного или с ошибкой) данные об использовании регистрируются:

  • prompt_tokens, completion_tokens, cached_tokens из ответа
  • cost_usd, рассчитанная на основе данных о ценах
  • latency_ms, status, error_class в случае ошибки
  • Сохраняются в таблице usage_history

Артефакты журнала вызовов (если эта функция включена) записываются в ${DATA_DIR}/call_logs/.


Основной обработчик запросов. Несмотря на свой размер, он имеет чёткую структуру:

// Псевдоструктура chatCore.ts
export async function handleChat(request: NextRequest) {
// 1. Аутентификация + CORS
await authenticateRequest(request);
applyCorsHeaders(response);
// 2. Проверка тела запроса
const body = await parseRequestBody(request);
// 3. Определение формата + преобразование
const sourceFormat = detectFormat(request);
const targetFormat = getTargetFormat(providerId);
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}
// 4. Маршрутизация комбо
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
const result = await executeOnTarget(target, body);
await recordUsage(result);
return result;
} catch (err) {
// Переход к следующей цели
}
}
// 5. Аварийный резервный вариант
return await emergencyFallback(body);
}

Несмотря на то что это одна гигантская функция, она организована в секции с комментариями, соответствующие пяти этапам конвейера.

Механизм маршрутизации, который преобразует комбо в упорядоченный список целей.

services/combo.ts
export async function handleComboChat(body, comboId): Promise<ChatResult> {
const targets = await resolveComboTargets(comboId, body);
for (const target of targets) {
try {
return await handleSingleModel(target, body);
} catch (err) {
log.warn("цель недоступна, пробуем следующую", { target, err });
}
}
throw new ComboExhaustedError("Все цели недоступны");
}

Поддерживает 19 стратегий маршрутизации (см. src/shared/constants/routingStrategies.ts):

Стратегия Поведение
priority Упорядоченный список с приоритетом первой цели
weighted Вероятностный выбор по весу каждой цели
round-robin Циклический перебор целей по порядку
context-relay Передача контекста между целями
fill-first Исчерпание квоты перед переходом к следующей цели
p2c Выбор лучшей из двух целей
random Равномерный случайный выбор
least-used Выбор цели с наименьшим количеством недавних обращений
cost-optimized Сначала выбирается самая дешёвая работоспособная цель
reset-aware Учёт периодов сброса лимитов провайдера
reset-window Маршрутизация на основе периода сброса
headroom Сначала выбирается цель с наибольшим остатком квоты
strict-random Полностью равномерный выбор (без взвешивания по качеству)
auto Использование оценки по 16 факторам (autoCombo/)
lkgp Сначала используется последний заведомо работоспособный провайдер
context-optimized Оптимальный выбор для запросов с длинным контекстом
fusion Параллельная отправка группе целей с последующим синтезом через судью (fusion.ts)

Абстрактный исполнитель, от которого наследуются все 107 исполнителей. Он содержит:

  • buildUrl() — стандартное построение URL (подклассы переопределяют для нестандартных случаев)
  • buildHeaders() — стандартные заголовки (аутентификация, тип содержимого)
  • transformRequest() — по умолчанию передаёт запрос без изменений
  • execute() — основной цикл HTTP-запросов с повторными попытками, задержкой и предохранителем
open-sse/executors/default.ts
export class DefaultExecutor extends BaseExecutor {
// Обрабатывает всех провайдеров, совместимых с OpenAI/Anthropic
// Провайдеры регистрируют конфигурации (URL, аутентификацию, заголовки), но используют общую логику исполнителя
}

Специфичное для провайдера поведение (заголовки аутентификации, базовый URL, заголовки версии) настраивается через реестр провайдеров, а не через отдельные классы исполнителей.

---
## Сервисы (117 модулей)
Сервисы — это **специализированные модули, предназначенные для одной конкретной задачи**, которые компонуются обработчиками. Основные категории:
### Маршрутизация и Combo
- `combo.ts` — точка входа для запросов с Combo-маршрутизацией
- `services/autoCombo/` — оценка по 16 факторам, 8 стратегий автоматической маршрутизации
- `wildcardRouter.ts` — сопоставляет маршруты с подстановочными знаками (`gpt-*`)
- `modelFamilyFallback.ts` — резервное переключение внутри семейства T5
### Ограничение частоты запросов и квоты
- `rateLimitManager.ts` — алгоритм корзины токенов для каждой пары ключ+провайдер
- `usage.ts` — регистрация использования
- `quotaCache.ts` — хранящиеся в памяти снимки квот
### Учётная запись и токен
- `tokenRefresh.ts` — обновление OAuth при ответе 401
- `accountFallback.ts` — переключение на альтернативную учётную запись
- `sessionManager.ts` — состояние многоходовой сессии
### Интеллектуальные функции
- `intentClassifier.ts` — классификация намерения запроса
- `taskAwareRouter.ts` — маршрутизация по типу задачи
- `thinkingBudget.ts` — распределение токенов рассуждения
- `contextManager.ts` — внедрение контекста маршрутизации
### Отказоустойчивость
- `resilience.ts` — оркестрация повторных попыток, задержек и автоматического выключателя
- `emergencyFallback.ts` — резервный вариант на крайний случай
- `modelDeprecation.ts` — автоматическая маршрутизация на модели-преемники
### Состояние
- `signatureCache.ts` — дедупликация по сигнатуре запроса
- `volumeDetector.ts` — сброс нагрузки
- `contextHandoff.ts` — сериализация сессии
### Сжатие
- `compression/` (подкаталог) — полный конвейер сжатия
- 39 файлов, охватывающих движки, наборы правил и адаптеры
### Навыки
- (описаны в [SKILLS.md](./SKILLS.md))
### Память
- (описана в [MEMORY.md](./MEMORY.md))
---
## Исполнители (75+ файлов)
По одному файлу на каждого провайдера. Все они расширяют `BaseExecutor` и переопределяют различающееся поведение.
### Общие шаблоны
Провайдеры разрешаются через `getExecutor(providerId)`, который возвращает настроенный исполнитель. Провайдеры, совместимые с OpenAI/Anthropic, используют `DefaultExecutor` (`executors/default.ts`). Специфичное для провайдера поведение (базовый URL, заголовки аутентификации, версия API) настраивается в `open-sse/config/providers/`, а преобразования тела запроса выполняются в `open-sse/translator/`.
**Пользовательский URL** задаётся через конфигурацию провайдера:
```ts
// Конфигурация провайдера в open-sse/config/providers/
export default {
id: "together",
baseURL: "https://api.together.xyz/v1/chat/completions",
}

Пользовательская аутентификация обрабатывается через конфигурацию аутентификации реестра провайдеров (ключ API, OAuth, профили заголовков).

Преобразования пользовательского тела запроса (например, отделение system от messages для Anthropic) регистрируются для каждого провайдера в open-sse/translator/.

### Фабрика исполнителей
`executors/index.ts` экспортирует `getExecutor(providerId)`:
```ts
import { getExecutor } from "@omniroute/open-sse/executors";
const executor = getExecutor("anthropic");
const result = await executor.execute({
model: "claude-sonnet-4-5",
messages: [...],
});

Разрешение выполняется через ExecutorRegistry (executors/registry.ts): каждый специализированный исполнитель объявляется во встроенной таблице executors/index.ts и регистрируется через registerExecutor(alias, instance) при загрузке модуля; getExecutor() обращается к реестру и использует мемоизированный DefaultExecutor в качестве резервного варианта для любого провайдера без специализированной записи. Полное сопоставление псевдонимов с исполнителями зафиксировано эталонным тестом tests/unit/executor-map-golden.test.ts.


Преобразование между 3 форматами: OpenAI, Anthropic, Gemini, а также новым Responses API.

import { needsTranslation, translateRequest } from "@omniroute/open-sse/translator";
if (needsTranslation(sourceFormat, targetFormat)) {
body = translateRequest(body, sourceFormat, targetFormat);
}

Распространённые преобразования:

  • OpenAI → Anthropic: отдельное поле system, заголовок x-api-key
  • OpenAI → Gemini: contents вместо messages, systemInstruction
  • OpenAI → Responses API: массив input, состояние previous_response_id
  • Роль developer → system для систем, отличных от OpenAI
  • Роль system → объединяется с первым сообщением пользователя для GLM/ERNIE
  • json_schema → responseMimeType + responseSchema в Gemini
  • tools → формат инструментов, специфичный для провайдера
  • Параметры рассуждения (o1, Claude) → эквиваленты, специфичные для провайдера

open-sse/mcp-server/ реализует сервер Model Context Protocol:

  • 110 инструментов (управление провайдерами, комбинации, память, кэш, сжатие, прокси, навыки, геймификация, плагины, Notion, Obsidian, локальный корпус)
  • 3 транспорта: stdio, SSE, Streamable HTTP
  • 33 области доступа для детального управления авторизацией

Инструменты регистрируются как отдельные файлы в open-sse/mcp-server/tools/, каждый из которых экспортирует имя, схему, обработчик и область доступа:

open-sse/mcp-server/tools/getHealth.ts
import { z } from "zod";
export default {
name: "omniroute_get_health",
description: "Получить снимок состояния системы",
scope: "read:health",
inputSchema: z.object({}),
handler: async (_args, ctx) => {
return await getSystemHealth();
},
};
// stdio (использование в CLI)
startMcpStdio(server);
// SSE (потоковая передача на основе HTTP)
startMcpSse(server, port);
// Streamable HTTP (современный MCP)
startMcpStreamable(server, port);

Каждый вызов инструмента проходит проверку областей доступа (open-sse/mcp-server/auth/):

if (!hasScope(apiKey, "providers:read")) {
throw new Error("Недостаточная область доступа");
}

open-sse/transformer/ выполняет преобразование между форматами Chat Completions и Responses API.

Responses API — это новый формат OpenAI с диалогами с сохранением состояния (previous_response_id). Когда клиент отправляет запрос Responses, OmniRoute:

  1. Внутренне преобразует Responses → Chat Completions
  2. Отправляет запрос провайдеру (любому провайдеру, поддерживающему Chat Completions)
  3. Преобразует ответ обратно в формат Responses
  4. Передаёт преобразованный ответ клиенту в потоковом режиме

Преобразователь (transformer/responsesTransformer.ts) предоставляет:

createResponsesApiTransformStream(): TransformStream

Он обрабатывает:

  • События response.output_item.added
  • События response.output_text.delta
  • Событие response.completed
  • Сопоставление вызовов инструментов (function_call ↔ tool_calls)

open-sse/config/ содержит слой конфигурации:

Файл Назначение
providerRegistry.ts Реестр чат-моделей на основе каталога из 352 провайдеров
providerModels.ts Псевдонимы моделей, сопоставление форматов
constants.ts Тайм-ауты, ограничения, коды состояния
defaultThinkingSignature.ts Сигнатура рассуждений Claude по умолчанию
modelStrip.ts (в сервисах) Удаление полей для отдельных провайдеров
interface ProviderConfig {
id: string;
name: string;
baseUrl: string;
authType: "bearer" | "api-key" | "oauth" | "cookie";
executorClass: string;
defaultModel: string;
capabilities: ProviderCapabilities;
models: ModelDefinition[];
}

Проверка с помощью Zod при загрузке модуля гарантирует корректность всех конфигураций провайдеров.


Механизм маршрутизации имеет строгие ограничения по производительности:

Операция Целевое значение Измерение
Разрешение комбинации <10ms Для 50 целей
Проверка ограничения частоты запросов <1ms Бакет токенов в памяти
Резервный выбор семейства моделей <5ms Кешированные определения семейств
Диспетчеризация маршрутизации запроса <2ms Критический путь
Без блокирующего ввода-вывода в критическом пути маршрутизации — Всё асинхронно

❌ Синхронные вызовы БД в combo.ts — выполняйте предварительные вычисления и кешируйте ❌ Логика повторных попыток в обработчиках — используйте retry() из сервиса обеспечения отказоустойчивости ❌ Прямой доступ к конфигурации провайдера — используйте геттеры providerRegistry ❌ Жёстко заданные цепочки резервного выбора — определяйте их в modelFamilyFallback.ts ❌ Изменения состояния между конкурентными запросами — используйте только контекст, ограниченный областью запроса


  1. Создайте open-sse/services/[serviceName].ts с чётко определённой зоной ответственности
  2. Экспортируйте основную функцию-обработчик и все необходимые константы
  3. Добавьте модульные тесты в tests/unit/services/[serviceName].test.mjs
  4. Интегрируйте сервис в конвейер обработки запросов в handlers/chatCore.ts (если он связан с маршрутизацией)
  5. Обновите логику маршрутизации в combo.ts, если сервис влияет на выбор цели
  6. Добавьте документацию в этот файл
  1. Создайте open-sse/executors/[provider].ts, расширяющий BaseExecutor
  2. Зарегистрируйте его в config/providerRegistry.ts
  3. Добавьте его в фабрику executors/index.ts
  4. Добавьте модульные тесты для исполнителя
  5. Добавьте документацию в docs/architecture/ARCHITECTURE.md
  1. Создайте или обновите open-sse/mcp-server/tools/[category]Tools.ts
  2. Определите схему Zod для входных данных
  3. Зарегистрируйте инструмент в mcp-server/index.ts
  4. Добавьте его в матрицу областей доступа в mcp-server/auth/
  5. Добавьте модульные тесты


Исходный код OmniRoute (a58000c7685f)

HagiCode

HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.

Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

Главный экран HagiCode в светлой теме
  • SmartСтруктурированные процессы превращают намерение в исполнимый путь от идеи до готового изменения.
  • EfficientМультиагентные процессы параллельно продвигают исследование, реализацию и проверку.
  • FunHero Dungeon делает длительную совместную разработку наглядной и увлекательной.
Перейти на HagiCode