Skip to content

Commit df24046

Browse files
igorcostaAutohand Evolve
andcommitted
Handle Ollama responses without message wrappers
Treat Ollama chat messages as optional so qwen3/template fallback responses do not crash when the API returns a bare completion payload. Add regression coverage for bare non-stream responses and bare streaming done chunks. Co-authored-by: Autohand Evolve <code-noreply@autohand.ai>
1 parent 19ce26d commit df24046

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

src/providers/OllamaProvider.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ interface OllamaRequestToolCall {
4242
}
4343

4444
interface OllamaChatResponse {
45-
message: {
45+
message?: {
4646
role: string;
4747
content: string;
4848
tool_calls?: OllamaToolCall[];
@@ -247,11 +247,12 @@ export class OllamaProvider implements LLMProvider {
247247
}
248248

249249
const data = await response.json() as OllamaChatResponse;
250+
const message = data.message ?? { role: 'assistant', content: '' };
250251

251252
// Parse tool calls if present (Ollama returns arguments as object, not string)
252253
let toolCalls: LLMToolCall[] | undefined;
253-
if (data.message.tool_calls && Array.isArray(data.message.tool_calls)) {
254-
toolCalls = data.message.tool_calls.map((tc: OllamaToolCall, index: number) => {
254+
if (message.tool_calls && Array.isArray(message.tool_calls)) {
255+
toolCalls = message.tool_calls.map((tc: OllamaToolCall, index: number) => {
255256
let argumentsStr: string;
256257
try {
257258
// Ollama returns arguments as object, convert to JSON string for consistency
@@ -281,7 +282,7 @@ export class OllamaProvider implements LLMProvider {
281282
return {
282283
id: `ollama-${Date.now()}`,
283284
created: Math.floor(new Date(data.created_at).getTime() / 1000),
284-
content: data.message.content,
285+
content: message.content,
285286
toolCalls,
286287
finishReason: toolCalls?.length ? 'tool_calls' : 'stop',
287288
usage,
@@ -550,7 +551,7 @@ export class OllamaProvider implements LLMProvider {
550551
for (const line of lines) {
551552
try {
552553
const data: OllamaChatResponse = JSON.parse(line);
553-
fullContent += data.message.content;
554+
fullContent += data.message?.content ?? '';
554555
lastData = data;
555556
// Ollama signals completion via the JSON "done" field
556557
if (data.done) {

tests/providers/OllamaProvider.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,25 @@ describe('OllamaProvider', () => {
155155
);
156156
});
157157

158+
it('handles bare Ollama chat responses without a message wrapper', async () => {
159+
const p = new OllamaProvider(config, { maxRetries: 0 });
160+
global.fetch = vi.fn().mockResolvedValue({
161+
ok: true,
162+
json: async () => ({
163+
created_at: '2024-11-21T10:30:00Z',
164+
done: true
165+
})
166+
});
167+
168+
const response = await p.complete({
169+
messages: [{ role: 'user', content: 'Hello' }]
170+
});
171+
172+
expect(response.content).toBe('');
173+
expect(response.toolCalls).toBeUndefined();
174+
expect(response.finishReason).toBe('stop');
175+
});
176+
158177
it('should handle streaming responses', async () => {
159178
const mockStream = new ReadableStream({
160179
start(controller) {
@@ -181,6 +200,30 @@ describe('OllamaProvider', () => {
181200
expect(response.content).toContain('Hello');
182201
});
183202

203+
it('honors bare Ollama stream chunks without a message wrapper', async () => {
204+
const mockStream = new ReadableStream({
205+
start(controller) {
206+
controller.enqueue(new TextEncoder().encode(
207+
'{"created_at":"2024-11-21T10:30:00Z","done":true}\n'
208+
));
209+
controller.close();
210+
}
211+
});
212+
213+
global.fetch = vi.fn().mockResolvedValue({
214+
ok: true,
215+
body: mockStream
216+
});
217+
218+
const response = await provider.complete({
219+
messages: [{ role: 'user', content: 'Hello' }],
220+
stream: true
221+
});
222+
223+
expect(response.content).toBe('');
224+
expect(response.finishReason).toBe('stop');
225+
});
226+
184227
// -----------------------------------------------------------------------
185228
// Error handling tests (TDD — these fail before the fix is implemented)
186229
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)