import { tap, expect } from '@git.zone/tstest/tapbundle'; import { createOllamaModel } from '../ts/smartai.provider.ollama.js'; import type { ISmartAiOptions } from '../ts/smartai.interfaces.js'; tap.test('createOllamaModel returns valid LanguageModelV3', async () => { const model = createOllamaModel({ provider: 'ollama', model: 'qwen3:8b', ollamaOptions: { think: true, num_ctx: 4096 }, }); expect(model.specificationVersion).toEqual('v3'); expect(model.provider).toEqual('ollama'); expect(model.modelId).toEqual('qwen3:8b'); expect(model).toHaveProperty('doGenerate'); expect(model).toHaveProperty('doStream'); }); tap.test('Qwen models get default temperature 0.55', async () => { // Mock fetch to capture the request body const originalFetch = globalThis.fetch; let capturedBody: Record | undefined; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { capturedBody = JSON.parse(init?.body as string); return new Response(JSON.stringify({ message: { content: 'test response', role: 'assistant' }, done: true, prompt_eval_count: 10, eval_count: 5, }), { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'qwen3:8b', }); await model.doGenerate({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], inputFormat: 'prompt', } as any); expect(capturedBody).toBeTruthy(); // Temperature 0.55 should be in the options expect((capturedBody!.options as Record).temperature).toEqual(0.55); } finally { globalThis.fetch = originalFetch; } }); tap.test('think option is passed at top level of request body', async () => { const originalFetch = globalThis.fetch; let capturedBody: Record | undefined; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { capturedBody = JSON.parse(init?.body as string); return new Response(JSON.stringify({ message: { content: 'test', role: 'assistant', thinking: 'let me think...' }, done: true, prompt_eval_count: 10, eval_count: 5, }), { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'qwen3:8b', ollamaOptions: { think: true, num_ctx: 4096 }, }); await model.doGenerate({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], inputFormat: 'prompt', } as any); expect(capturedBody).toBeTruthy(); // think should be at top level, not inside options expect(capturedBody!.think).toEqual(true); // num_ctx should be in options expect((capturedBody!.options as Record).num_ctx).toEqual(4096); } finally { globalThis.fetch = originalFetch; } }); tap.test('Non-qwen models do not get default temperature', async () => { const originalFetch = globalThis.fetch; let capturedBody: Record | undefined; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { capturedBody = JSON.parse(init?.body as string); return new Response(JSON.stringify({ message: { content: 'test', role: 'assistant' }, done: true, }), { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'llama3:8b', }); await model.doGenerate({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], inputFormat: 'prompt', } as any); expect(capturedBody).toBeTruthy(); // No temperature should be set expect((capturedBody!.options as Record).temperature).toBeUndefined(); } finally { globalThis.fetch = originalFetch; } }); tap.test('doGenerate parses reasoning/thinking from response', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { return new Response(JSON.stringify({ message: { content: 'The answer is 42.', role: 'assistant', thinking: 'Let me reason about this carefully...', }, done: true, prompt_eval_count: 20, eval_count: 15, }), { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'qwen3:8b', ollamaOptions: { think: true }, }); const result = await model.doGenerate({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'What is the meaning of life?' }] }], } as any); // Should have both reasoning and text content const reasoningParts = result.content.filter(c => c.type === 'reasoning'); const textParts = result.content.filter(c => c.type === 'text'); expect(reasoningParts.length).toEqual(1); expect((reasoningParts[0] as any).text).toEqual('Let me reason about this carefully...'); expect(textParts.length).toEqual(1); expect((textParts[0] as any).text).toEqual('The answer is 42.'); expect(result.finishReason.unified).toEqual('stop'); } finally { globalThis.fetch = originalFetch; } }); tap.test('doGenerate parses tool calls from response', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { return new Response(JSON.stringify({ message: { content: '', role: 'assistant', tool_calls: [ { function: { name: 'get_weather', arguments: { location: 'London', unit: 'celsius' }, }, }, ], }, done: true, prompt_eval_count: 30, eval_count: 10, }), { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'qwen3:8b', }); const result = await model.doGenerate({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'What is the weather in London?' }] }], tools: [{ type: 'function' as const, name: 'get_weather', description: 'Get weather for a location', inputSchema: { type: 'object', properties: { location: { type: 'string' }, unit: { type: 'string' }, }, }, }], } as any); const toolCalls = result.content.filter(c => c.type === 'tool-call'); expect(toolCalls.length).toEqual(1); expect((toolCalls[0] as any).toolName).toEqual('get_weather'); expect(JSON.parse((toolCalls[0] as any).input)).toEqual({ location: 'London', unit: 'celsius' }); expect(result.finishReason.unified).toEqual('tool-calls'); } finally { globalThis.fetch = originalFetch; } }); tap.test('doStream produces correct stream parts', async () => { const originalFetch = globalThis.fetch; // Simulate Ollama's newline-delimited JSON streaming const chunks = [ JSON.stringify({ message: { content: 'Hello', role: 'assistant' }, done: false }) + '\n', JSON.stringify({ message: { content: ' world', role: 'assistant' }, done: false }) + '\n', JSON.stringify({ message: { content: '!', role: 'assistant' }, done: true, prompt_eval_count: 5, eval_count: 3 }) + '\n', ]; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const encoder = new TextEncoder(); const stream = new ReadableStream({ start(controller) { for (const chunk of chunks) { controller.enqueue(encoder.encode(chunk)); } controller.close(); }, }); return new Response(stream, { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'llama3:8b', }); const result = await model.doStream({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], } as any); const parts: any[] = []; const reader = result.stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; parts.push(value); } // Should have: text-start, text-delta x3, text-end, finish const textDeltas = parts.filter(p => p.type === 'text-delta'); const finishParts = parts.filter(p => p.type === 'finish'); const textStarts = parts.filter(p => p.type === 'text-start'); const textEnds = parts.filter(p => p.type === 'text-end'); expect(textStarts.length).toEqual(1); expect(textDeltas.length).toEqual(3); expect(textDeltas.map((d: any) => d.delta).join('')).toEqual('Hello world!'); expect(textEnds.length).toEqual(1); expect(finishParts.length).toEqual(1); expect(finishParts[0].finishReason.unified).toEqual('stop'); } finally { globalThis.fetch = originalFetch; } }); tap.test('doStream handles thinking/reasoning in stream', async () => { const originalFetch = globalThis.fetch; const chunks = [ JSON.stringify({ message: { thinking: 'Let me think...', content: '', role: 'assistant' }, done: false }) + '\n', JSON.stringify({ message: { thinking: ' about this.', content: '', role: 'assistant' }, done: false }) + '\n', JSON.stringify({ message: { content: 'The answer.', role: 'assistant' }, done: false }) + '\n', JSON.stringify({ message: { content: '', role: 'assistant' }, done: true, prompt_eval_count: 10, eval_count: 8 }) + '\n', ]; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const encoder = new TextEncoder(); const stream = new ReadableStream({ start(controller) { for (const chunk of chunks) { controller.enqueue(encoder.encode(chunk)); } controller.close(); }, }); return new Response(stream, { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'qwen3:8b', ollamaOptions: { think: true }, }); const result = await model.doStream({ prompt: [{ role: 'user', content: [{ type: 'text', text: 'think about this' }] }], } as any); const parts: any[] = []; const reader = result.stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; parts.push(value); } const reasoningStarts = parts.filter(p => p.type === 'reasoning-start'); const reasoningDeltas = parts.filter(p => p.type === 'reasoning-delta'); const reasoningEnds = parts.filter(p => p.type === 'reasoning-end'); const textDeltas = parts.filter(p => p.type === 'text-delta'); expect(reasoningStarts.length).toEqual(1); expect(reasoningDeltas.length).toEqual(2); expect(reasoningDeltas.map((d: any) => d.delta).join('')).toEqual('Let me think... about this.'); expect(reasoningEnds.length).toEqual(1); expect(textDeltas.length).toEqual(1); expect(textDeltas[0].delta).toEqual('The answer.'); } finally { globalThis.fetch = originalFetch; } }); tap.test('message conversion handles system, assistant, and tool messages', async () => { const originalFetch = globalThis.fetch; let capturedBody: Record | undefined; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { capturedBody = JSON.parse(init?.body as string); return new Response(JSON.stringify({ message: { content: 'response', role: 'assistant' }, done: true, }), { status: 200 }); }; try { const model = createOllamaModel({ provider: 'ollama', model: 'llama3:8b', }); await model.doGenerate({ prompt: [ { role: 'system', content: 'You are helpful.' }, { role: 'user', content: [{ type: 'text', text: 'Hi' }] }, { role: 'assistant', content: [ { type: 'text', text: 'Let me check.' }, { type: 'tool-call', toolCallId: 'tc1', toolName: 'search', input: '{"q":"test"}' }, ], }, { role: 'tool', content: [ { type: 'tool-result', toolCallId: 'tc1', output: { type: 'text', value: 'result data' } }, ], }, { role: 'user', content: [{ type: 'text', text: 'What did you find?' }] }, ], } as any); const messages = capturedBody!.messages as Array>; expect(messages.length).toEqual(5); expect(messages[0].role).toEqual('system'); expect(messages[0].content).toEqual('You are helpful.'); expect(messages[1].role).toEqual('user'); expect(messages[1].content).toEqual('Hi'); expect(messages[2].role).toEqual('assistant'); expect(messages[2].content).toEqual('Let me check.'); expect((messages[2].tool_calls as any[]).length).toEqual(1); expect((messages[2].tool_calls as any[])[0].function.name).toEqual('search'); expect(messages[3].role).toEqual('tool'); expect(messages[3].content).toEqual('result data'); expect(messages[4].role).toEqual('user'); expect(messages[4].content).toEqual('What did you find?'); } finally { globalThis.fetch = originalFetch; } }); export default tap.start();