Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1936: VLLM client #6

Merged
merged 4 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- VLLM client and plugin to support VLLM communication.

## [0.0.7] - 2024-04-07

- Parse stream data from LLM correctly in relation to newlines
Expand Down
139 changes: 139 additions & 0 deletions src/Client/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

namespace Drupal\llm_services\Client;

use Drupal\llm_services\Exceptions\CommunicationException;
use Drupal\llm_services\Model\Payload;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;

/**
* Base client to communicate with a LLM framework.
*/
abstract class Client {

/**
* Cache for stream parsing.
*
* @var string
*
* @see parse()
*/
protected string $parserCache = '';

/**
* Default constructor.
*
* @param string $url
* The URL to the LLM serving framework.
* @param int $port
* The port that the framework is using.
* @param \GuzzleHttp\ClientInterface $client
* The http client used to interact with the framework.
* @param string $username
* Basic auth username (default: empty string).
* @param string $password
* Basic auth password (default: empty string).
* @param int $connectTimeout
* The timeout for connoting to the API.
* @param int $waitTimeout
* The timeout for wait on content from the API.
*/
public function __construct(
protected string $url,
protected int $port,
protected ClientInterface $client,
protected string $username = '',
protected string $password = '',
protected int $connectTimeout = 10,
protected int $waitTimeout = 300,
) {
}

/**
* Take all payload messages and change them into an array.
*
* This array of messages is used to give the model some chat context to make
* the interaction appear more like real char with a person.
*
* @param \Drupal\llm_services\Model\Payload $payload
* The payload sent to the chat function.
*
* @return array{content: string, role: string}[]
* Array of messages to send to the model.
*/
protected function chatMessagesAsArray(Payload $payload): array {
$messages = [];
foreach ($payload->getMessages() as $message) {
$messages[] = [
'content' => $message->content,
'role' => $message->role->value,
];
}

return $messages;
}

/**
* Make request to LLM framework.
*
* @param string $method
* The method to use (GET/POST).
* @param string $uri
* The API endpoint to call.
* @param array<string, mixed> $options
* Extra options and/or payload to post.
*
* @return \Psr\Http\Message\ResponseInterface
* The response object.
*
* @throws \Drupal\llm_services\Exceptions\CommunicationException
*/
protected function call(string $method, string $uri, array $options = []): ResponseInterface {
try {
// Add basic auth if given.
if (!empty($this->username)) {
$auth = 'Basic ' . base64_encode($this->username . ':' . $this->password);
if (isset($options['headers'])) {
$options['headers']['Authorization'] = $auth;
}
else {
$options['headers'] = ['Authorization' => $auth];
}
}

// Add default configuration options (shared between clients).
$options = array_merge([
RequestOptions::CONNECT_TIMEOUT => $this->connectTimeout,
RequestOptions::TIMEOUT => $this->waitTimeout,
RequestOptions::STREAM => TRUE,
], $options);

$response = $this->client->request($method, $this->getUrl($uri), $options);
if ($response->getStatusCode() !== 200) {
throw new CommunicationException('Request failed', $response->getStatusCode());
}
}
catch (GuzzleException $exception) {
throw new CommunicationException('Request failed: ' . $exception->getMessage(), $exception->getCode(), $exception);
}

return $response;
}

/**
* Returns a URL string with the given URI appended to the base URL.
*
* @param string $uri
* The URI to append to the base URL. Default is an empty string.
*
* @return string
* The complete URL string.
*/
protected function getUrl(string $uri = ''): string {
return $this->url . ':' . $this->port . ($uri ? '/' . ltrim($uri, '/') : '');
}

}
126 changes: 1 addition & 125 deletions src/Client/Ollama.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,12 @@

namespace Drupal\llm_services\Client;

use Drupal\llm_services\Exceptions\CommunicationException;
use Drupal\llm_services\Model\Payload;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;

/**
* Client to communicate with Ollama.
*/
class Ollama {

/**
* Cache for stream parsing.
*
* @var string
*
* @see parse()
*/
private string $parserCache = '';

/**
* Default constructor.
*
* @param string $url
* The URL of the Ollama server.
* @param int $port
* The port that Ollama is listening at.
* @param \GuzzleHttp\ClientInterface $client
* The http client used to interact with ollama.
* @param string $username
* Basic auth username (default: empty string).
* @param string $password
* Basic auth password (default: empty string).
*/
public function __construct(
private readonly string $url,
private readonly int $port,
private readonly ClientInterface $client,
private readonly string $username = '',
private readonly string $password = '',
) {
}
class Ollama extends Client {

/**
* List all models currently installed in Ollama.
Expand Down Expand Up @@ -95,9 +58,6 @@ public function install(string $modelName): \Generator {
'headers' => [
'Content-Type' => 'application/json',
],
RequestOptions::CONNECT_TIMEOUT => 10,
RequestOptions::TIMEOUT => 300,
RequestOptions::STREAM => TRUE,
]);

$body = $response->getBody();
Expand Down Expand Up @@ -132,9 +92,6 @@ public function completion(Payload $payload): \Generator {
'headers' => [
'Content-Type' => 'application/json',
],
RequestOptions::CONNECT_TIMEOUT => 10,
RequestOptions::TIMEOUT => 300,
RequestOptions::STREAM => TRUE,
]);

$body = $response->getBody();
Expand Down Expand Up @@ -167,9 +124,6 @@ public function chat(Payload $payload): \Generator {
'headers' => [
'Content-Type' => 'application/json',
],
RequestOptions::CONNECT_TIMEOUT => 10,
RequestOptions::TIMEOUT => 300,
RequestOptions::STREAM => TRUE,
]);

$body = $response->getBody();
Expand All @@ -179,32 +133,6 @@ public function chat(Payload $payload): \Generator {
}
}

/**
* Take all payload messages and change them into an array.
*
* This array of messages is used to give the model some chat context to make
* the interaction appear more like real char with a person.
*
* @param \Drupal\llm_services\Model\Payload $payload
* The payload sent to the chat function.
*
* @return array{content: string, role: string}[]
* Array of messages to send to Ollama.
*
* @see https://github.com/ollama/ollama/blob/main/docs/api.md#chat-request-with-history
*/
private function chatMessagesAsArray(Payload $payload): array {
$messages = [];
foreach ($payload->getMessages() as $message) {
$messages[] = [
'content' => $message->content,
'role' => $message->role->value,
];
}

return $messages;
}

/**
* Parse LLM stream.
*
Expand Down Expand Up @@ -345,56 +273,4 @@ private function parseDataToStrings(string $data): array {
return $results;
}

/**
* Make request to Ollama.
*
* @param string $method
* The method to use (GET/POST).
* @param string $uri
* The API endpoint to call.
* @param array<string, mixed> $options
* Extra options and/or payload to post.
*
* @return \Psr\Http\Message\ResponseInterface
* The response object.
*
* @throws \Drupal\llm_services\Exceptions\CommunicationException
*/
private function call(string $method, string $uri, array $options = []): ResponseInterface {
try {
// Add basic auth if given.
if (!empty($this->username)) {
$auth = 'Basic ' . base64_encode($this->username . ':' . $this->password);
if (isset($options['headers'])) {
$options['headers']['Authorization'] = $auth;
}
else {
$options['headers'] = ['Authorization' => $auth];
}
}
$response = $this->client->request($method, $this->getUrl($uri), $options);
if ($response->getStatusCode() !== 200) {
throw new CommunicationException('Request failed', $response->getStatusCode());
}
}
catch (GuzzleException $exception) {
throw new CommunicationException('Request failed: ' . $exception->getMessage(), $exception->getCode(), $exception);
}

return $response;
}

/**
* Returns a URL string with the given URI appended to the base URL.
*
* @param string $uri
* The URI to append to the base URL. Default is an empty string.
*
* @return string
* The complete URL string.
*/
private function getUrl(string $uri = ''): string {
return $this->url . ':' . $this->port . ($uri ? '/' . ltrim($uri, '/') : '');
}

}
8 changes: 7 additions & 1 deletion src/Client/OllamaChatResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ public function getRole(): MessageRoles {
}

/**
* {@inheritdoc}
* Get images.
*
* @todo Not sure yet but this must be an base64 encode image is on is sendt
* to the LLMs
*
* @return array<string>
* String of base64 encoded images.
*/
public function getImages(): array {
return $this->images;
Expand Down
Loading
Loading