Skip to content

Commit e658547

Browse files
committed
Ollama thinking
1 parent 85731e7 commit e658547

6 files changed

Lines changed: 519 additions & 27 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package com.chabicht.code_intelligence.apiclient;
2+
3+
import static com.chabicht.code_intelligence.model.ChatConversation.ChatOption.REASONING_EFFORT;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertFalse;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.io.IOException;
9+
import java.io.OutputStream;
10+
import java.net.InetSocketAddress;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.concurrent.CountDownLatch;
15+
import java.util.concurrent.TimeUnit;
16+
17+
import org.junit.jupiter.api.Test;
18+
19+
import com.chabicht.code_intelligence.chat.ChatSettings.ReasoningEffort;
20+
import com.chabicht.code_intelligence.model.ChatConversation;
21+
import com.chabicht.code_intelligence.model.ChatConversation.ChatListener;
22+
import com.chabicht.code_intelligence.model.ChatConversation.ChatMessage;
23+
import com.chabicht.code_intelligence.model.ChatConversation.Role;
24+
import com.google.gson.JsonArray;
25+
import com.google.gson.JsonObject;
26+
import com.google.gson.JsonParser;
27+
import com.sun.net.httpserver.HttpExchange;
28+
import com.sun.net.httpserver.HttpServer;
29+
30+
public class OllamaReasoningIntegrationTest {
31+
32+
@Test
33+
void performChatAddsThinkLevelForSupportedThinkingModel() throws Exception {
34+
try (ReasoningAwareOllamaServer server = new ReasoningAwareOllamaServer("0.17.7",
35+
jsonArray("completion", "tools", "thinking"))) {
36+
OllamaApiClient client = new OllamaApiClient(createConnection(server));
37+
ChatConversation chat = createChat(ReasoningEffort.MEDIUM);
38+
CountDownLatch responseFinished = new CountDownLatch(1);
39+
chat.addListener(createListener(responseFinished));
40+
41+
client.performChat("qwen3.5:35b", chat, 128);
42+
awaitChatFinished(client, responseFinished);
43+
44+
assertEquals(1, server.getChatRequests().size());
45+
assertEquals("medium", server.getChatRequests().get(0).get("think").getAsString());
46+
assertEquals(1, server.getVersionRequestCount());
47+
assertEquals(1, server.getShowRequestCount());
48+
}
49+
}
50+
51+
@Test
52+
void performChatAddsThinkFalseForExplicitOff() throws Exception {
53+
try (ReasoningAwareOllamaServer server = new ReasoningAwareOllamaServer("0.17.7",
54+
jsonArray("completion", "thinking"))) {
55+
OllamaApiClient client = new OllamaApiClient(createConnection(server));
56+
ChatConversation chat = createChat(ReasoningEffort.NONE);
57+
CountDownLatch responseFinished = new CountDownLatch(1);
58+
chat.addListener(createListener(responseFinished));
59+
60+
client.performChat("qwen3.5:35b", chat, 128);
61+
awaitChatFinished(client, responseFinished);
62+
63+
assertFalse(server.getChatRequests().get(0).get("think").getAsBoolean());
64+
}
65+
}
66+
67+
@Test
68+
void performChatOmitsThinkForModelDefaultWithoutProbing() throws Exception {
69+
try (ReasoningAwareOllamaServer server = new ReasoningAwareOllamaServer("0.17.7",
70+
jsonArray("completion", "thinking"))) {
71+
OllamaApiClient client = new OllamaApiClient(createConnection(server));
72+
ChatConversation chat = createChat(ReasoningEffort.DEFAULT);
73+
CountDownLatch responseFinished = new CountDownLatch(1);
74+
chat.addListener(createListener(responseFinished));
75+
76+
client.performChat("qwen3.5:35b", chat, 128);
77+
awaitChatFinished(client, responseFinished);
78+
79+
assertFalse(server.getChatRequests().get(0).has("think"));
80+
assertEquals(0, server.getVersionRequestCount());
81+
assertEquals(0, server.getShowRequestCount());
82+
}
83+
}
84+
85+
@Test
86+
void performChatOmitsThinkForOlderOllamaServers() throws Exception {
87+
try (ReasoningAwareOllamaServer server = new ReasoningAwareOllamaServer("0.8.9",
88+
jsonArray("completion", "thinking"))) {
89+
OllamaApiClient client = new OllamaApiClient(createConnection(server));
90+
ChatConversation chat = createChat(ReasoningEffort.HIGH);
91+
CountDownLatch responseFinished = new CountDownLatch(1);
92+
chat.addListener(createListener(responseFinished));
93+
94+
client.performChat("qwen3.5:35b", chat, 128);
95+
awaitChatFinished(client, responseFinished);
96+
97+
assertFalse(server.getChatRequests().get(0).has("think"));
98+
assertEquals(1, server.getVersionRequestCount());
99+
assertEquals(0, server.getShowRequestCount());
100+
}
101+
}
102+
103+
@Test
104+
void performChatOmitsThinkForModelsWithoutThinkingCapability() throws Exception {
105+
try (ReasoningAwareOllamaServer server = new ReasoningAwareOllamaServer("0.17.7", jsonArray("completion"))) {
106+
OllamaApiClient client = new OllamaApiClient(createConnection(server));
107+
ChatConversation chat = createChat(ReasoningEffort.HIGH);
108+
CountDownLatch responseFinished = new CountDownLatch(1);
109+
chat.addListener(createListener(responseFinished));
110+
111+
client.performChat("codellama:latest", chat, 128);
112+
awaitChatFinished(client, responseFinished);
113+
114+
assertFalse(server.getChatRequests().get(0).has("think"));
115+
assertEquals(1, server.getVersionRequestCount());
116+
assertEquals(1, server.getShowRequestCount());
117+
}
118+
}
119+
120+
private ChatConversation createChat(ReasoningEffort reasoningEffort) {
121+
ChatConversation chat = new ChatConversation();
122+
chat.getOptions().put(REASONING_EFFORT, reasoningEffort);
123+
chat.addMessage(new ChatMessage(Role.USER, "Count to one."), false);
124+
return chat;
125+
}
126+
127+
private ChatListener createListener(CountDownLatch responseFinished) {
128+
return new ChatListener() {
129+
@Override
130+
public void onMessageAdded(ChatMessage message, boolean updating) {
131+
}
132+
133+
@Override
134+
public void onMessageUpdated(ChatMessage message) {
135+
}
136+
137+
@Override
138+
public void onFunctionCall(ChatMessage message) {
139+
}
140+
141+
@Override
142+
public void onChatResponseFinished(ChatMessage message) {
143+
responseFinished.countDown();
144+
}
145+
};
146+
}
147+
148+
private void awaitChatFinished(OllamaApiClient client, CountDownLatch responseFinished) throws InterruptedException {
149+
assertTrue(responseFinished.await(10, TimeUnit.SECONDS), "Timed out waiting for chat response to finish");
150+
for (int i = 0; i < 40 && client.isChatPending(); i++) {
151+
Thread.sleep(50);
152+
}
153+
assertFalse(client.isChatPending(), "Client request should be complete after chat response finished");
154+
}
155+
156+
private JsonArray jsonArray(String... values) {
157+
JsonArray array = new JsonArray();
158+
for (String value : values) {
159+
array.add(value);
160+
}
161+
return array;
162+
}
163+
164+
private AiApiConnection createConnection(ReasoningAwareOllamaServer server) {
165+
AiApiConnection connection = new AiApiConnection();
166+
connection.setType(AiApiConnection.ApiType.OLLAMA);
167+
connection.setBaseUri(server.getBaseUri());
168+
connection.setEnabled(true);
169+
return connection;
170+
}
171+
172+
private static final class ReasoningAwareOllamaServer implements AutoCloseable {
173+
private static final String CHAT_RESPONSE = """
174+
{"model":"test","message":{"role":"assistant","content":"One"},"done":true}
175+
""";
176+
177+
private final HttpServer server;
178+
private final String version;
179+
private final JsonArray capabilities;
180+
private final List<JsonObject> chatRequests = new ArrayList<>();
181+
private int versionRequestCount;
182+
private int showRequestCount;
183+
184+
private ReasoningAwareOllamaServer(String version, JsonArray capabilities) throws IOException {
185+
this.version = version;
186+
this.capabilities = capabilities;
187+
server = HttpServer.create(new InetSocketAddress(0), 0);
188+
server.createContext("/api/version", this::handleVersion);
189+
server.createContext("/api/show", this::handleShow);
190+
server.createContext("/api/chat", this::handleChat);
191+
server.start();
192+
}
193+
194+
private void handleVersion(HttpExchange exchange) throws IOException {
195+
versionRequestCount++;
196+
writeJsonResponse(exchange, """
197+
{"version":"%s"}
198+
""".formatted(version));
199+
}
200+
201+
private void handleShow(HttpExchange exchange) throws IOException {
202+
showRequestCount++;
203+
exchange.getRequestBody().readAllBytes();
204+
JsonObject response = new JsonObject();
205+
response.add("capabilities", capabilities.deepCopy());
206+
writeJsonResponse(exchange, response.toString());
207+
}
208+
209+
private void handleChat(HttpExchange exchange) throws IOException {
210+
byte[] requestBytes = exchange.getRequestBody().readAllBytes();
211+
chatRequests.add(JsonParser.parseString(new String(requestBytes, StandardCharsets.UTF_8)).getAsJsonObject());
212+
writeJsonResponse(exchange, CHAT_RESPONSE.strip() + "\n");
213+
}
214+
215+
private void writeJsonResponse(HttpExchange exchange, String responseBody) throws IOException {
216+
byte[] responseBytes = responseBody.getBytes(StandardCharsets.UTF_8);
217+
exchange.getResponseHeaders().add("Content-Type", "application/json");
218+
exchange.sendResponseHeaders(200, responseBytes.length);
219+
try (OutputStream responseStream = exchange.getResponseBody()) {
220+
responseStream.write(responseBytes);
221+
}
222+
}
223+
224+
private String getBaseUri() {
225+
return "http://127.0.0.1:" + server.getAddress().getPort();
226+
}
227+
228+
private List<JsonObject> getChatRequests() {
229+
return chatRequests;
230+
}
231+
232+
private int getVersionRequestCount() {
233+
return versionRequestCount;
234+
}
235+
236+
private int getShowRequestCount() {
237+
return showRequestCount;
238+
}
239+
240+
@Override
241+
public void close() {
242+
server.stop(0);
243+
}
244+
}
245+
}

‎com.chabicht.code-intelligence.tests/src/com/chabicht/code_intelligence/chat/ChatSettingsTest.java‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.chabicht.code_intelligence.chat;
22

3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertFalse;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -18,10 +19,32 @@ void getReasoningControlModeUsesConnectionTypeFamilies() {
1819
assertEquals(ReasoningControlMode.EFFORT, ChatSettings.getReasoningControlMode(ApiType.OPENAI_RESPONSES));
1920
assertEquals(ReasoningControlMode.TOKEN_BUDGET, ChatSettings.getReasoningControlMode(ApiType.ANTHROPIC));
2021
assertEquals(ReasoningControlMode.TOKEN_BUDGET, ChatSettings.getReasoningControlMode(ApiType.GEMINI));
21-
assertEquals(ReasoningControlMode.NONE, ChatSettings.getReasoningControlMode(ApiType.OLLAMA));
22+
assertEquals(ReasoningControlMode.OLLAMA_EFFORT, ChatSettings.getReasoningControlMode(ApiType.OLLAMA));
2223
assertEquals(ReasoningControlMode.NONE, ChatSettings.getReasoningControlMode(ApiType.XAI));
2324
}
2425

26+
@Test
27+
void getSupportedReasoningEffortsRestrictsOllamaChoices() {
28+
assertArrayEquals(
29+
new ReasoningEffort[] { ReasoningEffort.DEFAULT, ReasoningEffort.NONE, ReasoningEffort.LOW,
30+
ReasoningEffort.MEDIUM, ReasoningEffort.HIGH },
31+
ChatSettings.getSupportedReasoningEfforts(ReasoningControlMode.OLLAMA_EFFORT));
32+
assertArrayEquals(ReasoningEffort.values(),
33+
ChatSettings.getSupportedReasoningEfforts(ReasoningControlMode.EFFORT));
34+
}
35+
36+
@Test
37+
void normalizeReasoningEffortClampsUnsupportedOllamaValues() {
38+
assertEquals(ReasoningEffort.LOW,
39+
ChatSettings.normalizeReasoningEffort(ReasoningControlMode.OLLAMA_EFFORT, ReasoningEffort.MINIMAL));
40+
assertEquals(ReasoningEffort.HIGH,
41+
ChatSettings.normalizeReasoningEffort(ReasoningControlMode.OLLAMA_EFFORT, ReasoningEffort.XHIGH));
42+
assertEquals(ReasoningEffort.DEFAULT,
43+
ChatSettings.normalizeReasoningEffort(ReasoningControlMode.OLLAMA_EFFORT, null));
44+
assertEquals(ReasoningEffort.HIGH,
45+
ChatSettings.normalizeReasoningEffort(ReasoningControlMode.EFFORT, ReasoningEffort.HIGH));
46+
}
47+
2548
@Test
2649
void isReasoningSupportedAndEnabledUsesExplicitEffortForOpenAiStyleConnections() {
2750
ChatSettings settings = new ChatSettings() {
@@ -56,4 +79,24 @@ public ReasoningControlMode getReasoningControlMode() {
5679
settings.setReasoningEnabled(true);
5780
assertTrue(settings.isReasoningSupportedAndEnabled());
5881
}
82+
83+
@Test
84+
void isReasoningSupportedAndEnabledUsesExplicitEffortForOllamaConnections() {
85+
ChatSettings settings = new ChatSettings() {
86+
@Override
87+
public ReasoningControlMode getReasoningControlMode() {
88+
return ReasoningControlMode.OLLAMA_EFFORT;
89+
}
90+
};
91+
92+
settings.setReasoningEffort(ReasoningEffort.DEFAULT);
93+
assertFalse(settings.isReasoningSupportedAndEnabled());
94+
95+
settings.setReasoningEffort(ReasoningEffort.NONE);
96+
assertTrue(settings.isReasoningSupportedAndEnabled());
97+
98+
settings.setReasoningEffort(ReasoningEffort.XHIGH);
99+
assertEquals(ReasoningEffort.HIGH, settings.getEffectiveReasoningEffort());
100+
assertTrue(settings.isReasoningSupportedAndEnabled());
101+
}
59102
}

0 commit comments

Comments
 (0)