Skip to content

Commit fc32bc6

Browse files
committed
feature #827 [Platform] Extensions to Ollama streaming (leonexcc)
This PR was merged into the main branch. Discussion ---------- [Platform] Extensions to Ollama streaming | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | no | Issues | None | License | MIT Adds the following things to the Ollama streaming feature: - Some testing - OllamaMessageChunk supports thinking (Ollama streams the tokens of the reasoning) - OllamaMessageChunk adds raw data (like stats and token usage information at the end of the stream) While I added this, I saw that most of the other ResultConverter::convertStream return just strings. Maybe it would be a good idea to make the API for this more consistent and allow string|MessageChunkInterface or only some MessageChunkInterface . That would make it probably easier to switch Platforms. Since this is neither a bug nor realy a new feature, I didn't check any above. I didn't find any docs about Ollama streaming so I didn't add anything there. If needed I can try to add something. I did add tests, though 😉. <!-- Replace this notice by a description of your feature/bugfix. This will help reviewers and should be a good start for the documentation. Additionally (see https://symfony.com/releases): - Always add tests and ensure they pass. - For new features, provide some code snippets to help understand usage. - Features and deprecations must be submitted against branch main. - Update/add documentation as required (we can help!) - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry - Never break backward compatibility (see https://symfony.com/bc). --> Commits ------- 7d071ec Extensions to OllamaMessageChunk and support of thinking
2 parents 6b70738 + 7d071ec commit fc32bc6

File tree

3 files changed

+85
-0
lines changed

3 files changed

+85
-0
lines changed

src/platform/src/Bridge/Ollama/OllamaMessageChunk.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ final class OllamaMessageChunk
1818
{
1919
/**
2020
* @param array<string, mixed> $message
21+
* @param array<string, mixed> $raw
2122
*/
2223
public function __construct(
2324
public readonly string $model,
2425
public readonly \DateTimeImmutable $created_at,
2526
public readonly array $message,
2627
public readonly bool $done,
28+
public readonly array $raw,
2729
) {
2830
}
2931

@@ -38,8 +40,18 @@ public function getContent(): ?string
3840
return $this->message['content'] ?? null;
3941
}
4042

43+
public function getThinking(): ?string
44+
{
45+
return $this->message['thinking'] ?? null;
46+
}
47+
4148
public function getRole(): ?string
4249
{
4350
return $this->message['role'] ?? null;
4451
}
52+
53+
public function isDone(): bool
54+
{
55+
return $this->done;
56+
}
4557
}

src/platform/src/Bridge/Ollama/OllamaResultConverter.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ private function convertStream(RawResultInterface $result): \Generator
106106
new \DateTimeImmutable($data['created_at']),
107107
$data['message'],
108108
$data['done'],
109+
$data,
109110
);
110111
}
111112
}

src/platform/tests/Bridge/Ollama/OllamaResultConverterTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\AI\Platform\Bridge\Ollama\Ollama;
16+
use Symfony\AI\Platform\Bridge\Ollama\OllamaMessageChunk;
1617
use Symfony\AI\Platform\Bridge\Ollama\OllamaResultConverter;
1718
use Symfony\AI\Platform\Exception\RuntimeException;
1819
use Symfony\AI\Platform\Model;
1920
use Symfony\AI\Platform\Result\InMemoryRawResult;
2021
use Symfony\AI\Platform\Result\RawHttpResult;
22+
use Symfony\AI\Platform\Result\StreamResult;
2123
use Symfony\AI\Platform\Result\TextResult;
2224
use Symfony\AI\Platform\Result\ToolCallResult;
2325
use Symfony\Contracts\HttpClient\ResponseInterface;
@@ -160,4 +162,74 @@ public function testItConvertsAResponseToAVectorResult()
160162
$this->assertSame([0.3, 0.4, 0.4], $convertedContent[0]->getData());
161163
$this->assertSame([0.0, 0.0, 0.2], $convertedContent[1]->getData());
162164
}
165+
166+
public function testConvertStreamingResponse()
167+
{
168+
$converter = new OllamaResultConverter();
169+
$rawResult = new InMemoryRawResult(dataStream: $this->generateConvertStreamingStream());
170+
171+
$result = $converter->convert($rawResult, options: ['stream' => true]);
172+
173+
$this->assertInstanceOf(StreamResult::class, $result);
174+
175+
$chunks = $result->getContent();
176+
$this->assertInstanceOf(OllamaMessageChunk::class, $chunks->current());
177+
$this->assertSame('Hello', $chunks->current()->getContent());
178+
$this->assertFalse($chunks->current()->isDone());
179+
$this->assertSame('deepseek-r1:latest', $chunks->current()->raw['model']);
180+
$this->assertArrayNotHasKey('total_duration', $chunks->current()->raw);
181+
$chunks->next();
182+
$this->assertInstanceOf(OllamaMessageChunk::class, $chunks->current());
183+
$this->assertSame(' world!', $chunks->current()->getContent());
184+
$this->assertTrue($chunks->current()->isDone());
185+
$this->assertArrayHasKey('total_duration', $chunks->current()->raw);
186+
}
187+
188+
public function testConvertThinkingStreamingResponse()
189+
{
190+
$converter = new OllamaResultConverter();
191+
$rawResult = new InMemoryRawResult(dataStream: $this->generateConvertThinkingStreamingStream());
192+
193+
$result = $converter->convert($rawResult, options: ['stream' => true]);
194+
195+
$this->assertInstanceOf(StreamResult::class, $result);
196+
197+
$chunks = $result->getContent();
198+
$this->assertInstanceOf(OllamaMessageChunk::class, $chunks->current());
199+
$this->assertSame('', $chunks->current()->getContent());
200+
$this->assertSame('Thinking', $chunks->current()->getThinking());
201+
$this->assertFalse($chunks->current()->isDone());
202+
$this->assertSame('deepseek-r1:latest', $chunks->current()->raw['model']);
203+
$this->assertArrayNotHasKey('total_duration', $chunks->current()->raw);
204+
$chunks->next();
205+
$this->assertSame('', $chunks->current()->getContent());
206+
$this->assertSame(' hard', $chunks->current()->getThinking());
207+
$this->assertFalse($chunks->current()->isDone());
208+
$chunks->next();
209+
$this->assertSame('Hello', $chunks->current()->getContent());
210+
$this->assertNull($chunks->current()->getThinking());
211+
$this->assertFalse($chunks->current()->isDone());
212+
$chunks->next();
213+
$this->assertInstanceOf(OllamaMessageChunk::class, $chunks->current());
214+
$this->assertSame(' world!', $chunks->current()->getContent());
215+
$this->assertNull($chunks->current()->getThinking());
216+
$this->assertTrue($chunks->current()->isDone());
217+
$this->assertArrayHasKey('total_duration', $chunks->current()->raw);
218+
}
219+
220+
private function generateConvertStreamingStream(): iterable
221+
{
222+
yield ['model' => 'deepseek-r1:latest', 'created_at' => '2025-10-29T17:15:49.631700779Z', 'message' => ['role' => 'assistant', 'content' => 'Hello'], 'done' => false];
223+
yield ['model' => 'deepseek-r1:latest', 'created_at' => '2025-10-29T17:15:49.905924913Z', 'message' => ['role' => 'assistant', 'content' => ' world!'], 'done' => true,
224+
'done_reason' => 'stop', 'total_duration' => 100, 'load_duration' => 10, 'prompt_eval_count' => 42, 'prompt_eval_duration' => 30, 'eval_count' => 17, 'eval_duration' => 60];
225+
}
226+
227+
private function generateConvertThinkingStreamingStream(): iterable
228+
{
229+
yield ['model' => 'deepseek-r1:latest', 'created_at' => '2025-10-29T17:15:49.631700779Z', 'message' => ['role' => 'assistant', 'content' => '', 'thinking' => 'Thinking'], 'done' => false];
230+
yield ['model' => 'deepseek-r1:latest', 'created_at' => '2025-10-29T17:15:49.905924913Z', 'message' => ['role' => 'assistant', 'content' => '', 'thinking' => ' hard'], 'done' => false];
231+
yield ['model' => 'deepseek-r1:latest', 'created_at' => '2025-10-29T17:15:50.14497475Z', 'message' => ['role' => 'assistant', 'content' => 'Hello'], 'done' => false];
232+
yield ['model' => 'deepseek-r1:latest', 'created_at' => '2025-10-29T17:15:50.367912083Z', 'message' => ['role' => 'assistant', 'content' => ' world!'], 'done' => true,
233+
'done_reason' => 'stop', 'total_duration' => 100, 'load_duration' => 10, 'prompt_eval_count' => 42, 'prompt_eval_duration' => 30, 'eval_count' => 17, 'eval_duration' => 60];
234+
}
163235
}

0 commit comments

Comments
 (0)