mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-09-15 02:56:10 +08:00
llm: sanitize malformed request Unicode
This commit is contained in:
@@ -166,7 +166,9 @@ class ModelService(
|
|||||||
extraBody?.forEach { (key, value) ->
|
extraBody?.forEach { (key, value) ->
|
||||||
requestJson[key] = value
|
requestJson[key] = value
|
||||||
}
|
}
|
||||||
return JsonObject(requestJson).toString()
|
// Ktor's text writer rejects lone UTF-16 surrogates. They can enter chat history through
|
||||||
|
// malformed gateway text, so normalize only invalid code units before encoding the body.
|
||||||
|
return JsonObject(requestJson).toString().replaceUnpairedSurrogates()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun decodeStreamChunk(rawJson: String): ChatCompletionChunk? {
|
internal fun decodeStreamChunk(rawJson: String): ChatCompletionChunk? {
|
||||||
@@ -237,6 +239,31 @@ class ModelService(
|
|||||||
if (normalized.length <= maxChars) normalized else normalized.take(maxChars) + "...[截断]"
|
if (normalized.length <= maxChars) normalized else normalized.take(maxChars) + "...[截断]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.replaceUnpairedSurrogates(): String {
|
||||||
|
var output: StringBuilder? = null
|
||||||
|
var index = 0
|
||||||
|
while (index < length) {
|
||||||
|
val current = this[index]
|
||||||
|
when {
|
||||||
|
Character.isHighSurrogate(current) &&
|
||||||
|
index + 1 < length && Character.isLowSurrogate(this[index + 1]) -> {
|
||||||
|
output?.append(current)?.append(this[index + 1])
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
Character.isSurrogate(current) -> {
|
||||||
|
if (output == null) output = StringBuilder(length).append(this, 0, index)
|
||||||
|
output.append('\uFFFD')
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
output?.append(current)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output?.toString() ?: this
|
||||||
|
}
|
||||||
|
|
||||||
private fun isSafetyRejection(code: String?, message: String?): Boolean {
|
private fun isSafetyRejection(code: String?, message: String?): Boolean {
|
||||||
val normalizedCode = code.orEmpty().lowercase()
|
val normalizedCode = code.orEmpty().lowercase()
|
||||||
val normalizedMessage = message.orEmpty().lowercase()
|
val normalizedMessage = message.orEmpty().lowercase()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
|||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
@@ -250,6 +251,48 @@ class ModelServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendsRequestsContainingUnpairedSurrogates() = runBlocking {
|
||||||
|
val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
|
||||||
|
val receivedBody = AtomicReference<String>()
|
||||||
|
val responseBody = listOf(
|
||||||
|
"""data: {"id":"chunk-1","object":"chat.completion.chunk","created":1,"model":"test-model","choices":[{"index":0,"delta":{"content":"ok"},"finish_reason":null}]}""",
|
||||||
|
"data: [DONE]",
|
||||||
|
"",
|
||||||
|
).joinToString("\n\n").toByteArray()
|
||||||
|
server.createContext("/v1/chat/completions") { exchange ->
|
||||||
|
receivedBody.set(String(exchange.requestBody.readBytes(), Charsets.UTF_8))
|
||||||
|
exchange.responseHeaders.add("Content-Type", "text/event-stream")
|
||||||
|
exchange.sendResponseHeaders(200, responseBody.size.toLong())
|
||||||
|
exchange.responseBody.use { body -> body.write(responseBody) }
|
||||||
|
}
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
val localService = ModelService(
|
||||||
|
baseUrl = "http://127.0.0.1:${server.address.port}/v1/",
|
||||||
|
token = "test",
|
||||||
|
timeout = 1.seconds,
|
||||||
|
firstChunkTimeout = 1.seconds,
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
val content = StringBuilder()
|
||||||
|
localService.chatCompletions(
|
||||||
|
ChatCompletionRequest(
|
||||||
|
model = ModelId("test-model"),
|
||||||
|
messages = listOf(ChatMessage.User("emoji \uD83D\uDE00 before\uD800middle\uDC00after")),
|
||||||
|
)
|
||||||
|
).collect { chunk ->
|
||||||
|
chunk.choices.firstOrNull()?.delta?.content?.let(content::append)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("ok", content.toString())
|
||||||
|
assertTrue(receivedBody.get().contains("emoji \uD83D\uDE00 before\uFFFDmiddle\uFFFDafter"))
|
||||||
|
} finally {
|
||||||
|
localService.httpClient.close()
|
||||||
|
server.stop(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun queuesFlowsBeforeStartingWork() = runBlocking {
|
fun queuesFlowsBeforeStartingWork() = runBlocking {
|
||||||
val active = AtomicInteger()
|
val active = AtomicInteger()
|
||||||
|
|||||||
Reference in New Issue
Block a user