feat: выравнивание контракта конца SSE — дописывать data: [DONE] при штатном закрытии
Build LLM Proxy / Build and push (release) Successful in 38s
Build LLM Proxy / Build and push (release) Successful in 38s
Проблема (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.
This commit is contained in:
+5
-1
@@ -17,6 +17,7 @@
|
||||
| `ThinkTagTransformTest` | Non-stream путь `transformThinkMessage`: перенос рассуждений в `reasoning_content`, дописывание к уже имеющемуся, strip, незакрытый блок, отсутствие изменений → null; |
|
||||
| `ThinkTagChunkTest` | SSE-чанки `transformThinkChunk`: удержание хвоста тега между чанками, независимые сплиттеры по index, удаление пустого `content`; |
|
||||
| `ThinkTagStreamTest` | Обвязка стрима `streamSseWithThinkTags`: разрез тега между data-событиями, сброс удержанного хвоста в финиш-чанке, прохождение служебных строк и `[DONE]`, битый JSON, чанк без choices, strip. |
|
||||
| `StreamDoneContractTest` | Контракт конца SSE: детектор `finish_reason`/`[DONE]` (в т.ч. разрезанных границей чтения), дописывание `data: [DONE]\n\n` в сыром passthrough и в think-обвязке при штатном закрытии без маркера, отсутствие маркера при обрыве без `finish_reason`, отсутствие дублирования. |
|
||||
|
||||
## Проверка качества тестов (мутационная приёмка)
|
||||
|
||||
@@ -36,7 +37,10 @@
|
||||
- `transformThinkChunk` возвращает null → падают тесты чанков и стрима;
|
||||
- блок финиш-чанка в `streamSseWithThinkTags` не выполняется → падает тест про удержанный хвост;
|
||||
- `holdableSuffix` всегда 0 (хвост тега не удерживается) → падают тесты автомата, чанков и стрима;
|
||||
- `strip` начинает отдавать рассуждения → падает тест автомата.
|
||||
- `strip` начинает отдавать рассуждения → падает тест автомата;
|
||||
- `missingDoneMarker` всегда `false` → падают тесты сырого пути (`rawStreamIsByteExactAndAppendsDone`) и тест разрезанного маркера;
|
||||
- `if (sawFinishReason && !sawDone)` → `if (!sawDone)` в `streamSseWithThinkTags` → падает `thinkStreamTruncatedNeedsNoMarker`;
|
||||
- `if (sawFinishReason && !sawDone)` → `if (false)` в `streamSseWithThinkTags` → падает `thinkStreamAppendsDoneWhenUpstreamClosedWithoutMarker`.
|
||||
|
||||
Правило: боевой код нельзя подгонять под тест; если тест не проходит, неверен тест.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user