timeout(); $baseUrl = $this->baseUrl(); try { $response = Http::connectTimeout(5) ->timeout($timeout) ->withOptions(['read_timeout' => $timeout]) ->post($baseUrl.'/v1/embeddings', [ 'model' => $this->model('embedding', (string) config('services.llm.embedding_model')), 'input' => $text, ]) ->throw() ->json(); } catch (Throwable $e) { throw $this->mapException($e, 'embedding'); } $embedding = $response['data'][0]['embedding'] ?? []; if (! is_array($embedding) || $embedding === []) { throw new OllamaUnavailableException('lmstudio', 'embedding', 'No embedding in response'); } return $embedding; } public function generate(string $prompt, array $options = []): string { $timeout = $this->timeout(); $baseUrl = $this->baseUrl(); $expectJson = (bool) ($options['expect_json'] ?? false); $task = (string) ($options['task'] ?? 'chat'); $payload = [ 'model' => $this->model($task, (string) config('services.llm.chat_model')), 'messages' => [ ['role' => 'user', 'content' => $prompt], ], 'temperature' => 0.1, ]; if ($expectJson) { // OpenAI-compatible endpoints vary: some support json_schema/text only. $payload['response_format'] = ['type' => 'json_schema']; } try { $response = Http::connectTimeout(5) ->timeout($timeout) ->withOptions(['read_timeout' => $timeout]) ->post($baseUrl.'/v1/chat/completions', $payload) ->throw() ->json(); } catch (RequestException $e) { $body = (string) $e->response->body(); $isResponseFormatError = str_contains($body, 'response_format.type') || str_contains($body, 'json_schema'); if ($expectJson && $isResponseFormatError) { // Fallback retry without response_format for stricter local servers. try { $retryPayload = $payload; unset($retryPayload['response_format']); $response = Http::connectTimeout(5) ->timeout($timeout) ->withOptions(['read_timeout' => $timeout]) ->post($baseUrl.'/v1/chat/completions', $retryPayload) ->throw() ->json(); return (string) ($response['choices'][0]['message']['content'] ?? ''); } catch (Throwable $retryException) { throw $this->mapException($retryException, 'generation'); } } throw $this->mapException($e, 'generation'); } catch (Throwable $e) { throw $this->mapException($e, 'generation'); } return (string) ($response['choices'][0]['message']['content'] ?? ''); } private function baseUrl(): string { $instance = $this->settings->activeProviderInstance(); return rtrim((string) ($instance['base_url'] ?? $this->settings->get('llm.providers.lmstudio.base_url', (string) config('services.llm.base_url'))), '/'); } private function timeout(): int { return (int) $this->settings->get('llm.timeout', (string) config('services.llm.timeout', 30)); } private function model(string $task, string $fallback): string { $instance = $this->settings->activeProviderInstance(); $chatModel = (string) ($instance['chat_model'] ?? $this->settings->get('llm.providers.lmstudio.chat_model', $fallback)); $embeddingModel = (string) ($instance['embedding_model'] ?? $this->settings->get('llm.providers.lmstudio.embedding_model', $fallback)); if ($task === 'chat') { return $chatModel; } $configured = trim((string) $this->settings->get('llm.models.'.$task, '')); if ($configured !== '') { return $configured; } return $task === 'embedding' ? $embeddingModel : $chatModel; } private function mapException(Throwable $e, string $operation): OllamaUnavailableException { if ($e instanceof RequestException) { $body = $e->response->body(); $snippet = $body ? mb_substr($body, 0, 280) : null; return new OllamaUnavailableException('lmstudio', $operation, $e->getMessage(), $e, $snippet); } return new OllamaUnavailableException('lmstudio', $operation, $e->getMessage(), $e); } }