Add first-chunk timeout

This commit is contained in:
2026-05-22 14:16:00 +08:00
parent e629d37fa8
commit 72892336bc
3 changed files with 42 additions and 15 deletions
+34 -14
View File
@@ -14,6 +14,7 @@ import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.*
import kotlin.time.Duration
@@ -21,15 +22,16 @@ class ModelService(
val baseUrl: String,
val token: String,
val timeout: Duration,
val firstChunkTimeout: Duration,
val extraBody: JsonObject? = null
) {
val httpClient: HttpClient by lazy {
HttpClient(OkHttp) {
install(HttpTimeout) {
val millis = timeout.inWholeMilliseconds
requestTimeoutMillis = millis
connectTimeoutMillis = millis
socketTimeoutMillis = millis
// 总请求/socket 超时保持长值,允许慢速流式输出;连接握手则用短超时。
requestTimeoutMillis = timeout.inWholeMilliseconds
socketTimeoutMillis = timeout.inWholeMilliseconds
connectTimeoutMillis = firstChunkTimeout.inWholeMilliseconds
}
defaultRequest {
url(baseUrl)
@@ -66,17 +68,35 @@ class ModelService(
}.let { response ->
val channel: ByteReadChannel = response.body()
try {
while (currentCoroutineContext().isActive && !channel.isClosedForRead) {
val line = channel.readUTF8Line() ?: continue
when {
line.startsWith("data: [DONE]") -> break
line.startsWith("data: ") -> {
val chunk = json.decodeFromString<ChatCompletionChunk>(
line.removePrefix("data: ")
)
emit(chunk)
// 首块 data: 必须在 firstChunkTimeout 内到达,否则抛 TimeoutCancellationException
// 走 JChatGPT 的重试流程;之后的流式读取不再有应用层超时,由 socketTimeoutMillis 兜底。
val firstDataLine: String? = withTimeout(firstChunkTimeout) {
var found: String? = null
while (currentCoroutineContext().isActive && !channel.isClosedForRead) {
val line = channel.readUTF8Line() ?: continue
if (line.startsWith("data: ")) {
found = line
break
}
// 心跳/空行/注释行,不计为首块,继续等
}
found
}
if (firstDataLine != null) {
if (!firstDataLine.startsWith("data: [DONE]")) {
emit(json.decodeFromString(firstDataLine.removePrefix("data: ")))
while (currentCoroutineContext().isActive && !channel.isClosedForRead) {
val line = channel.readUTF8Line() ?: continue
when {
line.startsWith("data: [DONE]") -> break
line.startsWith("data: ") -> {
emit(json.decodeFromString(line.removePrefix("data: ")))
}
else -> continue
}
}
else -> continue
}
}
} finally {