first commit
This commit is contained in:
148
src/Infrastructure/OllamaClient.php
Normal file
148
src/Infrastructure/OllamaClient.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure;
|
||||
|
||||
use Generator;
|
||||
use JsonException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* OllamaClient
|
||||
*
|
||||
* Production-ready streaming client for Ollama-compatible LLM backends.
|
||||
*
|
||||
* Key properties:
|
||||
* - True live streaming (tokens are yielded while the request is running)
|
||||
* - PHP-safe (no yield inside cURL callbacks)
|
||||
* - Works for both HTTP streaming and CLI usage
|
||||
* - Deterministic and resource-safe
|
||||
*
|
||||
* Implementation strategy:
|
||||
* - Use curl_multi_* to keep control of the execution loop
|
||||
* - Accumulate partial chunks into a rolling buffer
|
||||
* - Extract JSON lines incrementally
|
||||
* - Yield tokens immediately when they arrive
|
||||
*/
|
||||
final class OllamaClient
|
||||
{
|
||||
private string $apiUrl;
|
||||
private string $model;
|
||||
private int $timeoutSeconds;
|
||||
|
||||
public function __construct(
|
||||
string $apiUrl,
|
||||
string $model,
|
||||
int $timeoutSeconds,
|
||||
) {
|
||||
$this->apiUrl = $apiUrl;
|
||||
$this->model = $model;
|
||||
$this->timeoutSeconds = $timeoutSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams tokens from the LLM backend in real time.
|
||||
*
|
||||
* @param string $prompt Fully constructed prompt
|
||||
*
|
||||
* @return Generator<string>
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function stream(string $prompt): Generator
|
||||
{
|
||||
$payload = json_encode([
|
||||
'model' => $this->model,
|
||||
'prompt' => $prompt,
|
||||
'stream' => true,
|
||||
], JSON_THROW_ON_ERROR);
|
||||
|
||||
$buffer = '';
|
||||
$done = false;
|
||||
|
||||
$ch = curl_init($this->apiUrl);
|
||||
if ($ch === false) {
|
||||
throw new RuntimeException('Failed to initialize cURL');
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_RETURNTRANSFER => false,
|
||||
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||||
CURLOPT_WRITEFUNCTION => function ($curl, string $data) use (&$buffer, &$done): int {
|
||||
$buffer .= $data;
|
||||
return strlen($data);
|
||||
},
|
||||
]);
|
||||
|
||||
$mh = curl_multi_init();
|
||||
if ($mh === false) {
|
||||
curl_close($ch);
|
||||
throw new RuntimeException('Failed to initialize cURL multi handle');
|
||||
}
|
||||
|
||||
curl_multi_add_handle($mh, $ch);
|
||||
|
||||
try {
|
||||
do {
|
||||
// Execute the multi handle
|
||||
do {
|
||||
$status = curl_multi_exec($mh, $running);
|
||||
} while ($status === CURLM_CALL_MULTI_PERFORM);
|
||||
|
||||
// Read incoming data from the buffer
|
||||
while (($pos = strpos($buffer, "\n")) !== false) {
|
||||
$line = trim(substr($buffer, 0, $pos));
|
||||
$buffer = substr($buffer, $pos + 1);
|
||||
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$json = json_decode($line, true, flags: JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($json['response'])) {
|
||||
yield $json['response'];
|
||||
}
|
||||
|
||||
if (!empty($json['done'])) {
|
||||
$done = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for network activity
|
||||
if ($running) {
|
||||
curl_multi_select($mh, 0.2);
|
||||
}
|
||||
} while ($running && !$done);
|
||||
|
||||
// Flush remaining buffer (edge case)
|
||||
if (!$done && trim($buffer) !== '') {
|
||||
try {
|
||||
$json = json_decode(trim($buffer), true, flags: JSON_THROW_ON_ERROR);
|
||||
if (isset($json['response'])) {
|
||||
yield $json['response'];
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
$error = curl_error($ch);
|
||||
throw new RuntimeException('LLM connection error: ' . $error);
|
||||
}
|
||||
} finally {
|
||||
curl_multi_remove_handle($mh, $ch);
|
||||
curl_multi_close($mh);
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user