Skip to content

Commit a467199

Browse files
authored
feat: add support for OpenRouter & Generic Models (#203)
* Added support for OpenRouter & Generic Models * Fixed linting --------- Co-authored-by: rglozman <[email protected]>
1 parent 80fd016 commit a467199

File tree

6 files changed

+174
-0
lines changed

6 files changed

+174
-0
lines changed

.env

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ AZURE_OPENAI_DEPLOYMENT=
2121
AZURE_OPENAI_VERSION=
2222
AZURE_OPENAI_KEY=
2323

24+
# For using OpenRouter
25+
OPENROUTER_KEY=
26+
2427
# For using SerpApi (tool)
2528
SERP_API_KEY=
2629

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ $embeddings = new Embeddings();
6363
* [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
6464
* [Anthropic's Claude](https://www.anthropic.com/claude) with [Anthropic](https://www.anthropic.com/) as Platform
6565
* [Meta's Llama](https://www.llama.com/) with [Ollama](https://ollama.com/) and [Replicate](https://replicate.com/) as Platform
66+
* [Google's Gemini](https://gemini.google.com/) with [OpenRouter](https://www.openrouter.com/) as Platform
67+
* [DeepSeek's R1](https://www.deepseek.com/) with [OpenRouter](https://www.openrouter.com/) as Platform
6668
* Embeddings Models
6769
* [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
6870
* [Voyage's Embeddings](https://docs.voyageai.com/docs/embeddings) with [Voyage](https://www.voyageai.com/) as Platform
@@ -125,6 +127,7 @@ $response = $chain->call($messages, [
125127
1. **OpenAI's o1**: [chat-o1-openai.php](examples/chat-o1-openai.php)
126128
1. **Meta's Llama with Ollama**: [chat-llama-ollama.php](examples/chat-llama-ollama.php)
127129
1. **Meta's Llama with Replicate**: [chat-llama-replicate.php](examples/chat-llama-replicate.php)
130+
1. **Google's Gemini with OpenRouter**: [chat-gemini-openrouter.php](examples/chat-gemini-openrouter.php)
128131

129132
### Tools
130133

examples/chat-gemini-openrouter.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Bridge\OpenRouter\GenericModel;
4+
use PhpLlm\LlmChain\Bridge\OpenRouter\PlatformFactory;
5+
use PhpLlm\LlmChain\Chain;
6+
use PhpLlm\LlmChain\Model\Message\Message;
7+
use PhpLlm\LlmChain\Model\Message\MessageBag;
8+
use Symfony\Component\Dotenv\Dotenv;
9+
10+
require_once dirname(__DIR__).'/vendor/autoload.php';
11+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
12+
13+
if (empty($_ENV['OPENROUTER_KEY'])) {
14+
echo 'Please set the OPENROUTER_KEY environment variable.'.PHP_EOL;
15+
exit(1);
16+
}
17+
18+
$platform = PlatformFactory::create($_ENV['OPENROUTER_KEY']);
19+
$llm = new GenericModel('google/gemini-2.0-flash-thinking-exp:free');
20+
21+
$chain = new Chain($platform, $llm);
22+
$messages = new MessageBag(
23+
Message::forSystem('You are a helpful assistant.'),
24+
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
25+
);
26+
$response = $chain->call($messages);
27+
28+
echo $response->getContent().PHP_EOL;

src/Bridge/OpenRouter/Client.php

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenRouter;
6+
7+
use PhpLlm\LlmChain\Exception\RuntimeException;
8+
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
9+
use PhpLlm\LlmChain\Model\Model;
10+
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
11+
use PhpLlm\LlmChain\Model\Response\TextResponse;
12+
use PhpLlm\LlmChain\Platform\ModelClient;
13+
use PhpLlm\LlmChain\Platform\ResponseConverter;
14+
use Symfony\Component\HttpClient\EventSourceHttpClient;
15+
use Symfony\Contracts\HttpClient\HttpClientInterface;
16+
use Symfony\Contracts\HttpClient\ResponseInterface;
17+
use Webmozart\Assert\Assert;
18+
19+
final readonly class Client implements ModelClient, ResponseConverter
20+
{
21+
private EventSourceHttpClient $httpClient;
22+
23+
public function __construct(
24+
HttpClientInterface $httpClient,
25+
#[\SensitiveParameter] private string $apiKey,
26+
) {
27+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
28+
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
29+
Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".');
30+
}
31+
32+
public function supports(Model $model, array|string|object $input): bool
33+
{
34+
return $input instanceof MessageBagInterface;
35+
}
36+
37+
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
38+
{
39+
return $this->httpClient->request('POST', 'https://openrouter.ai/api/v1/chat/completions', [
40+
'auth_bearer' => $this->apiKey,
41+
'json' => array_merge($options, [
42+
'model' => $model->getVersion(),
43+
'messages' => $input,
44+
]),
45+
]);
46+
}
47+
48+
public function convert(ResponseInterface $response, array $options = []): LlmResponse
49+
{
50+
$data = $response->toArray();
51+
52+
if (!isset($data['choices'][0]['message'])) {
53+
throw new RuntimeException('Response does not contain message');
54+
}
55+
56+
if (!isset($data['choices'][0]['message']['content'])) {
57+
throw new RuntimeException('Message does not contain content');
58+
}
59+
60+
return new TextResponse($data['choices'][0]['message']['content']);
61+
}
62+
}
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenRouter;
6+
7+
use PhpLlm\LlmChain\Bridge\Meta\Llama;
8+
use PhpLlm\LlmChain\Model\LanguageModel;
9+
10+
final readonly class GenericModel implements LanguageModel
11+
{
12+
/**
13+
* @param array<string, mixed> $options
14+
*/
15+
public function __construct(
16+
private string $version = Llama::LLAMA_3_2_90B_VISION_INSTRUCT,
17+
private array $options = [],
18+
) {
19+
}
20+
21+
public function getVersion(): string
22+
{
23+
return $this->version;
24+
}
25+
26+
public function getOptions(): array
27+
{
28+
return $this->options;
29+
}
30+
31+
public function supportsAudioInput(): bool
32+
{
33+
return false; // it does, but implementation here is still open.
34+
}
35+
36+
public function supportsImageInput(): bool
37+
{
38+
return false; // it does, but implementation here is still open.
39+
}
40+
41+
public function supportsStreaming(): bool
42+
{
43+
return false; // it does, but implementation here is still open.
44+
}
45+
46+
public function supportsToolCalling(): bool
47+
{
48+
return false; // it does, but implementation here is still open.
49+
}
50+
51+
public function supportsStructuredOutput(): bool
52+
{
53+
return false; // it does, but implementation here is still open.
54+
}
55+
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenRouter;
6+
7+
use PhpLlm\LlmChain\Platform;
8+
use Symfony\Component\HttpClient\EventSourceHttpClient;
9+
use Symfony\Contracts\HttpClient\HttpClientInterface;
10+
11+
final class PlatformFactory
12+
{
13+
public static function create(
14+
#[\SensitiveParameter]
15+
string $apiKey,
16+
?HttpClientInterface $httpClient = null,
17+
): Platform {
18+
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
19+
$handler = new Client($httpClient, $apiKey);
20+
21+
return new Platform([$handler], [$handler]);
22+
}
23+
}

0 commit comments

Comments
 (0)