baseUrl = rtrim((string) ($cfg->ollama_base_url ?? 'http://localhost:11434'), '/'); $this->timeout = (int) ($cfg->ollama_timeout ?? 90); $this->retries = (int) ($cfg->ollama_retries ?? 1); } /** * POST /api/chat — synchronous, non-streaming. * * @param string $model e.g. "nibiru-coder:1.0" * @param array $messages [{role: 'user'|'assistant'|'system', content: '...'}] * @param array $options {temperature, num_predict, top_p, ...} * @return array decoded JSON response */ public function chat(string $model, array $messages, array $options = []): array { return $this->post('/api/chat', [ 'model' => $model, 'messages' => $messages, 'stream' => false, 'options' => $options, ]); } /** * POST /api/embeddings — single input. * * @return array{embedding: float[]} */ public function embed(string $model, string $prompt): array { return $this->post('/api/embeddings', [ 'model' => $model, 'prompt' => $prompt, ]); } /** * POST /api/generate — single-prompt completion (no chat structure). */ public function generate(string $model, string $prompt, array $options = []): array { return $this->post('/api/generate', [ 'model' => $model, 'prompt' => $prompt, 'stream' => false, 'options' => $options, ]); } /** * GET /api/tags — list available models. */ public function listModels(): array { return $this->get('/api/tags'); } /** * GET /api/ps — list currently-loaded models. */ public function listLoaded(): array { return $this->get('/api/ps'); } /** * POST /api/pull — pull a model onto the Ollama server. */ public function pull(string $name): array { return $this->post('/api/pull', ['name' => $name, 'stream' => false]); } /** * POST /api/create — register a custom model from a Modelfile. */ public function create(string $name, string $modelfile): array { return $this->post('/api/create', [ 'name' => $name, 'modelfile' => $modelfile, 'stream' => false, ]); } // ----------------------------------------------------------------------- // HTTP helpers // ----------------------------------------------------------------------- protected function post(string $path, array $body): array { $url = $this->baseUrl . $path; $payload = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); return $this->withRetries(function () use ($url, $payload) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_CONNECTTIMEOUT => 10, ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($body === false) { throw new \RuntimeException("Ollama transport error: $err"); } if ($code >= 400) { $snip = substr((string) $body, 0, 240); throw new \RuntimeException("Ollama HTTP $code: $snip"); } $decoded = json_decode((string) $body, true); if (!is_array($decoded)) { throw new \RuntimeException('Ollama returned non-JSON body.'); } return $decoded; }); } protected function get(string $path): array { $ch = curl_init($this->baseUrl . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 5, ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) { throw new \RuntimeException("Ollama GET $path failed (HTTP $code)"); } return json_decode((string) $body, true) ?? []; } protected function withRetries(callable $fn) { $last = null; for ($i = 0; $i <= $this->retries; $i++) { try { return $fn(); } catch (\RuntimeException $e) { $last = $e; if ($i < $this->retries) usleep(250_000 * (1 << $i)); // 250ms, 500ms, ... } } throw $last ?? new \RuntimeException('Ollama call failed.'); } }