Skip to content

Commit fca7ac2

Browse files
authored
fix(prompt): accept text/plain Content-Type on Ollama non-streaming responses (#1887)
Ollama sometimes replies to non-streaming `/api/chat` requests with `Content-Type: text/plain; charset=utf-8` even though the body is valid JSON. Ktor's `ContentNegotiation` plugin was only registered for `application/json`, so the response failed with `NoTransformationFoundException` before reaching `OllamaChatResponseDTO`. ### Change Register the same JSON deserializer for `text/plain` alongside `application/json` in the Ollama client's `ContentNegotiation` config. Streaming responses are unaffected — they read the body as a raw channel and bypass content negotiation. ### Test - Extended `MockOllamaChatServer` to allow overriding the response `Content-Type`. - Added `OllamaContentTypeTest` that sends a `text/plain; charset=utf-8` reply and asserts the body is still parsed into an assistant message. The test fails on `develop` without the fix and passes with it. closes #1237
1 parent bb801c9 commit fca7ac2

3 files changed

Lines changed: 50 additions & 1 deletion

File tree

prompt/prompt-executor/prompt-executor-clients/prompt-executor-ollama-client/src/commonMain/kotlin/ai/koog/prompt/executor/ollama/client/OllamaClient.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ public class OllamaClient @JvmOverloads constructor(
151151
}
152152
install(ContentNegotiation) {
153153
json(ollamaJson)
154+
// Ollama sometimes returns non-streaming responses with `Content-Type: text/plain`
155+
// (see https://github.com/JetBrains/koog/issues/1237). Register the same JSON
156+
// deserializer for that content type so the body is still parsed correctly.
157+
json(ollamaJson, ContentType.Text.Plain)
154158
}
155159
install(HttpTimeout) {
156160
requestTimeoutMillis = timeoutConfig.requestTimeoutMillis

prompt/prompt-executor/prompt-executor-clients/prompt-executor-ollama-client/src/commonTest/kotlin/ai/koog/prompt/executor/ollama/client/MockOllamaChatServer.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.ktor.http.headersOf
1212
import kotlinx.serialization.json.Json
1313

1414
internal class MockOllamaChatServer(
15+
private val contentType: String = "application/json",
1516
private val handler: (OllamaChatRequestDTO) -> OllamaChatResponseDTO,
1617
) {
1718
val mockEngine = MockEngine.Companion { requestData ->
@@ -20,7 +21,7 @@ internal class MockOllamaChatServer(
2021
respond(
2122
content = Json.encodeToString<OllamaChatResponseDTO>(response),
2223
status = HttpStatusCode.Companion.OK,
23-
headers = headersOf(HttpHeaders.ContentType to listOf("application/json")),
24+
headers = headersOf(HttpHeaders.ContentType to listOf(contentType)),
2425
)
2526
}
2627

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package ai.koog.prompt.executor.ollama.client
2+
3+
import ai.koog.prompt.dsl.prompt
4+
import ai.koog.prompt.executor.ollama.client.dto.OllamaChatMessageDTO
5+
import ai.koog.prompt.executor.ollama.client.dto.OllamaChatResponseDTO
6+
import ai.koog.prompt.message.Message
7+
import io.ktor.client.HttpClient
8+
import kotlinx.coroutines.test.runTest
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
import kotlin.test.assertIs
12+
13+
/**
14+
* Regression tests for https://github.com/JetBrains/koog/issues/1237.
15+
*
16+
* Ollama can reply to a non-streaming chat request with `Content-Type: text/plain; charset=utf-8`
17+
* even though the body is valid JSON. The client must still be able to deserialize such responses.
18+
*/
19+
class OllamaContentTypeTest {
20+
21+
@Test
22+
fun `test non-streaming chat response with text-plain content type is parsed`() = runTest {
23+
val responseContent = "Hello from Ollama"
24+
25+
val mockServer = MockOllamaChatServer(contentType = "text/plain; charset=utf-8") { request ->
26+
OllamaChatResponseDTO(
27+
model = request.model,
28+
message = OllamaChatMessageDTO(role = "assistant", content = responseContent),
29+
done = true
30+
)
31+
}
32+
33+
val ollamaClient = OllamaClient(baseClient = HttpClient(mockServer.mockEngine))
34+
35+
val responses = ollamaClient.execute(
36+
prompt = prompt("test") { user("Hi") },
37+
model = OllamaModels.Meta.LLAMA_3_2
38+
)
39+
40+
assertEquals(1, responses.size)
41+
val assistant = assertIs<Message.Assistant>(responses.first())
42+
assertEquals(responseContent, assistant.content)
43+
}
44+
}

0 commit comments

Comments
 (0)