Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Added support for OpenRouter & Generic Models #203

Merged
merged 2 commits into from
Feb 5, 2025
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
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ AZURE_OPENAI_DEPLOYMENT=
AZURE_OPENAI_VERSION=
AZURE_OPENAI_KEY=

# For using OpenRouter
OPENROUTER_KEY=

# For using SerpApi (tool)
SERP_API_KEY=

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ $embeddings = new Embeddings();
* [OpenAI's GPT](https://platform.openai.com/docs/models/overview) with [OpenAI](https://platform.openai.com/docs/overview) and [Azure](https://learn.microsoft.com/azure/ai-services/openai/concepts/models) as Platform
* [Anthropic's Claude](https://www.anthropic.com/claude) with [Anthropic](https://www.anthropic.com/) as Platform
* [Meta's Llama](https://www.llama.com/) with [Ollama](https://ollama.com/) and [Replicate](https://replicate.com/) as Platform
* [Google's Gemini](https://gemini.google.com/) with [OpenRouter](https://www.openrouter.com/) as Platform
* [DeepSeek's R1](https://www.deepseek.com/) with [OpenRouter](https://www.openrouter.com/) as Platform
* Embeddings Models
* [OpenAI's Text Embeddings](https://platform.openai.com/docs/guides/embeddings/embedding-models) with [OpenAI](https://platform.openai.com/docs/overview) and [Azure](https://learn.microsoft.com/azure/ai-services/openai/concepts/models) as Platform
* [Voyage's Embeddings](https://docs.voyageai.com/docs/embeddings) with [Voyage](https://www.voyageai.com/) as Platform
Expand Down Expand Up @@ -125,6 +127,7 @@ $response = $chain->call($messages, [
1. **OpenAI's o1**: [chat-o1-openai.php](examples/chat-o1-openai.php)
1. **Meta's Llama with Ollama**: [chat-llama-ollama.php](examples/chat-llama-ollama.php)
1. **Meta's Llama with Replicate**: [chat-llama-replicate.php](examples/chat-llama-replicate.php)
1. **Google's Gemini with OpenRouter**: [chat-gemini-openrouter.php](examples/chat-gemini-openrouter.php)

### Tools

Expand Down
28 changes: 28 additions & 0 deletions examples/chat-gemini-openrouter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use PhpLlm\LlmChain\Bridge\OpenRouter\GenericModel;
use PhpLlm\LlmChain\Bridge\OpenRouter\PlatformFactory;
use PhpLlm\LlmChain\Chain;
use PhpLlm\LlmChain\Model\Message\Message;
use PhpLlm\LlmChain\Model\Message\MessageBag;
use Symfony\Component\Dotenv\Dotenv;

require_once dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');

if (empty($_ENV['OPENROUTER_KEY'])) {
echo 'Please set the OPENROUTER_KEY environment variable.'.PHP_EOL;
exit(1);
}

$platform = PlatformFactory::create($_ENV['OPENROUTER_KEY']);
$llm = new GenericModel('google/gemini-2.0-flash-thinking-exp:free');

$chain = new Chain($platform, $llm);
$messages = new MessageBag(
Message::forSystem('You are a helpful assistant.'),
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
);
$response = $chain->call($messages);

echo $response->getContent().PHP_EOL;
62 changes: 62 additions & 0 deletions src/Bridge/OpenRouter/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Bridge\OpenRouter;

use PhpLlm\LlmChain\Exception\RuntimeException;
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
use PhpLlm\LlmChain\Model\Model;
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
use PhpLlm\LlmChain\Model\Response\TextResponse;
use PhpLlm\LlmChain\Platform\ModelClient;
use PhpLlm\LlmChain\Platform\ResponseConverter;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Webmozart\Assert\Assert;

final readonly class Client implements ModelClient, ResponseConverter
{
private EventSourceHttpClient $httpClient;

public function __construct(
HttpClientInterface $httpClient,
#[\SensitiveParameter] private string $apiKey,
) {
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".');
}

public function supports(Model $model, array|string|object $input): bool
{
return $input instanceof MessageBagInterface;
}

public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
{
return $this->httpClient->request('POST', 'https://openrouter.ai/api/v1/chat/completions', [
'auth_bearer' => $this->apiKey,
'json' => array_merge($options, [
'model' => $model->getVersion(),
'messages' => $input,
]),
]);
}

public function convert(ResponseInterface $response, array $options = []): LlmResponse
{
$data = $response->toArray();

if (!isset($data['choices'][0]['message'])) {
throw new RuntimeException('Response does not contain message');
}

if (!isset($data['choices'][0]['message']['content'])) {
throw new RuntimeException('Message does not contain content');
}

return new TextResponse($data['choices'][0]['message']['content']);
}
}
55 changes: 55 additions & 0 deletions src/Bridge/OpenRouter/GenericModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Bridge\OpenRouter;

use PhpLlm\LlmChain\Bridge\Meta\Llama;
use PhpLlm\LlmChain\Model\LanguageModel;

final readonly class GenericModel implements LanguageModel
{
/**
* @param array<string, mixed> $options
*/
public function __construct(
private string $version = Llama::LLAMA_3_2_90B_VISION_INSTRUCT,
private array $options = [],
) {
}

public function getVersion(): string
{
return $this->version;
}

public function getOptions(): array
{
return $this->options;
}

public function supportsAudioInput(): bool
{
return false; // it does, but implementation here is still open.
}

public function supportsImageInput(): bool
{
return false; // it does, but implementation here is still open.
}

public function supportsStreaming(): bool
{
return false; // it does, but implementation here is still open.
}

public function supportsToolCalling(): bool
{
return false; // it does, but implementation here is still open.
}

public function supportsStructuredOutput(): bool
{
return false; // it does, but implementation here is still open.
}
}
23 changes: 23 additions & 0 deletions src/Bridge/OpenRouter/PlatformFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Bridge\OpenRouter;

use PhpLlm\LlmChain\Platform;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class PlatformFactory
{
public static function create(
#[\SensitiveParameter]
string $apiKey,
?HttpClientInterface $httpClient = null,
): Platform {
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
$handler = new Client($httpClient, $apiKey);

return new Platform([$handler], [$handler]);
}
}