subochev 558b2f925b
Build LLM Proxy / Build and push (release) Successful in 38s
feat: выравнивание контракта конца SSE — дописывать data: [DONE] при штатном закрытии
Проблема (Vikunja #71): opencode показывал «модель ещё думает» ~45 с после конца
генерации на апстриме MiniMax (модель codding-big), который НЕ присылает
терминатор data: [DONE] и просто закрывает сокет. Bifrost для custom-провайдеров
считает маркер обязательным, поэтому завершал стрим клиенту только на закрытии
keep-alive-сокета llm-proxy (Ktor CIO connectionIdleTimeoutSeconds = 45).

Правка: стрим клиенту завершается маркером data: [DONE] всегда, когда апстрим
закрыл поток штатно (в потоке был непустой finish_reason), но сам маркера не
прислал. Обрыв БЕЗ finish_reason маркер НЕ дописывает — иначе усечённый стрим
выглядит как успешный (детект Bifrost SSEStreamEndedOnMarker).

- Main.kt: SSE_DONE_MARKER, StreamEndDetector (скользящее окно 128 байт — маркер
  может разрезаться границей чтения), hasNonNullFinishReasonText (экранированные
  вхождения не считаются), hasFinishReason, streamRawWithDoneContract для ветки
  think_tags: off; в streamSseWithThinkTags — учёт sawDone/sawFinishReason и
  дописывание маркера после сброса хвостов сплиттеров.
- StreamDoneContractTest.kt: 13 тестов (детектор, разрез маркера границей чтения,
  byte-exact passthrough, отсутствие дублирования, обрыв без finish_reason).
- TESTING.md: строка про новый тест-файл + 3 пункта мутационной приёмки;
- docs/sse-done-contract.md: ТЗ и разбор замеров.

Проверено: ./gradlew clean jvmTest fatJar — 79 тестов (было 66), 0 падений;
мутационная приёмка — ослабление охраны до `if (!sawDone)` роняет
thinkStreamTruncatedNeedsNoMarker.
2026-09-11 18:26:52 +03:00
fix
2026-08-30 14:06:35 +03:00

llm-proxy

Прозрачная прослойка/роутер OpenAI API. Принимает /v1/chat/completions и /v1/models, маршрутизирует между объявленными в YAML апстримами по внутреннему учёту конкурентности и мержит patch-слои (provider → upstream → model) в тело запроса. Подробная спецификация конфига — в CONFIG.md.

Конфиг

Конфигурация — в YAML-файле (по умолчанию config.yaml в каталоге проекта, CWD; переопределяется env CONFIG_PATH). Блоки: server (интерфейс биндинга host + port, дефолты 0.0.0.0 / 8100), providers, upstreams, models. Старый механизм env-переменных (UPSTREAM_URL, ROUTER_API_KEY, EXCLUDED_PROVIDERS, THINKING_MODELS, PORT) удалён — его поведение теперь в декларативном patch и блоке upstreams, а порт/интерфейс — в блоке server.

Лимит конкурентности max_concurrency можно задать и на провайдере (лимит по умолчанию для его апстримов), и на апстриме (перекрывает провайдерский); без обоих — безлимит.

Обработку think-тегов включает опциональный флажок think_tags у провайдера или апстрима (off по умолчанию, split — рассуждения из <think>…</think> уходят в reasoning_content, strip — выбрасываются); работает и в стриме, и в non-stream.

Переменная Default Описание
CONFIG_PATH config.yaml (CWD) Путь к YAML-конфигу
Ключ конфига Default Описание
server.host 0.0.0.0 Интерфейс/адрес биндинга
server.port 8100 Порт сервера

Таймаут на запрос к нейронке (весь ответ, включая стриминг) — константа UPSTREAM_REQUEST_TIMEOUT_MS = 5 минут; без неё дефолт CIO-движка Ktor — 15 секунд, и длинные генерации обрываются. Значение логируется при старте.

Сборка и деплой

  • CI (Gitea Actions, на release): ./gradlew fatJar → образ images.binom.pw/llm-proxy:<tag> (zot).
  • Запуск на 76.179 (llm-router): podman-compose up -d из podman-compose.yaml (образ тянется из images.binom.pw, сеть bifrost_default — туда же Bifrost ходит по имени llm-proxy).
  • Bifrost: в config_providers провайдера Routerai network_config_json.base_url = http://llm-proxy:8100 (после смены — рестарт bifrost_gateway).
S
Description
OpenAI-прослойка перед RouterAI (provider.ignore + no-think)
Readme 625 KiB
14 Latest
2026-09-14 22:06:15 +00:00
Languages
Kotlin 99.9%