From da697c23bb3435c9f866671f18565472fc72ea21 Mon Sep 17 00:00:00 2001 From: Ron Glozman Date: Fri, 31 Jan 2025 12:48:09 -0500 Subject: [PATCH 1/2] Added support for OpenRouter & Generic Models --- .env | 3 ++ README.md | 3 ++ examples/chat-gemini-openrouter.php | 28 ++++++++++ src/Bridge/OpenRouter/Client.php | 64 +++++++++++++++++++++++ src/Bridge/OpenRouter/GenericModel.php | 56 ++++++++++++++++++++ src/Bridge/OpenRouter/PlatformFactory.php | 23 ++++++++ 6 files changed, 177 insertions(+) create mode 100644 examples/chat-gemini-openrouter.php create mode 100644 src/Bridge/OpenRouter/Client.php create mode 100644 src/Bridge/OpenRouter/GenericModel.php create mode 100644 src/Bridge/OpenRouter/PlatformFactory.php diff --git a/.env b/.env index 1a6a7e78..a0fba9e9 100644 --- a/.env +++ b/.env @@ -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= diff --git a/README.md b/README.md index 6691625d..2bab0e0e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/examples/chat-gemini-openrouter.php b/examples/chat-gemini-openrouter.php new file mode 100644 index 00000000..aaf93b09 --- /dev/null +++ b/examples/chat-gemini-openrouter.php @@ -0,0 +1,28 @@ +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; diff --git a/src/Bridge/OpenRouter/Client.php b/src/Bridge/OpenRouter/Client.php new file mode 100644 index 00000000..b3759c50 --- /dev/null +++ b/src/Bridge/OpenRouter/Client.php @@ -0,0 +1,64 @@ +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']); + } + +} diff --git a/src/Bridge/OpenRouter/GenericModel.php b/src/Bridge/OpenRouter/GenericModel.php new file mode 100644 index 00000000..600b84b5 --- /dev/null +++ b/src/Bridge/OpenRouter/GenericModel.php @@ -0,0 +1,56 @@ + $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. + } +} diff --git a/src/Bridge/OpenRouter/PlatformFactory.php b/src/Bridge/OpenRouter/PlatformFactory.php new file mode 100644 index 00000000..233184a4 --- /dev/null +++ b/src/Bridge/OpenRouter/PlatformFactory.php @@ -0,0 +1,23 @@ + Date: Tue, 4 Feb 2025 01:31:53 +0000 Subject: [PATCH 2/2] Fixed linting --- src/Bridge/OpenRouter/Client.php | 30 +++++++++++------------ src/Bridge/OpenRouter/GenericModel.php | 1 - src/Bridge/OpenRouter/PlatformFactory.php | 2 +- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Bridge/OpenRouter/Client.php b/src/Bridge/OpenRouter/Client.php index b3759c50..4d349c8e 100644 --- a/src/Bridge/OpenRouter/Client.php +++ b/src/Bridge/OpenRouter/Client.php @@ -4,17 +4,17 @@ 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; -use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse; -use PhpLlm\LlmChain\Model\Response\TextResponse; -use PhpLlm\LlmChain\Exception\RuntimeException; final readonly class Client implements ModelClient, ResponseConverter { @@ -45,20 +45,18 @@ public function request(Model $model, object|array|string $input, array $options ]); } - - 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'); - } + public function convert(ResponseInterface $response, array $options = []): LlmResponse + { + $data = $response->toArray(); - if (!isset($data['choices'][0]['message']['content'])) { - throw new RuntimeException('Message does not contain content'); - } + if (!isset($data['choices'][0]['message'])) { + throw new RuntimeException('Response does not contain message'); + } - return new TextResponse($data['choices'][0]['message']['content']); - } + if (!isset($data['choices'][0]['message']['content'])) { + throw new RuntimeException('Message does not contain content'); + } + return new TextResponse($data['choices'][0]['message']['content']); + } } diff --git a/src/Bridge/OpenRouter/GenericModel.php b/src/Bridge/OpenRouter/GenericModel.php index 600b84b5..65d32e59 100644 --- a/src/Bridge/OpenRouter/GenericModel.php +++ b/src/Bridge/OpenRouter/GenericModel.php @@ -9,7 +9,6 @@ final readonly class GenericModel implements LanguageModel { - /** * @param array $options */ diff --git a/src/Bridge/OpenRouter/PlatformFactory.php b/src/Bridge/OpenRouter/PlatformFactory.php index 233184a4..da36ebbc 100644 --- a/src/Bridge/OpenRouter/PlatformFactory.php +++ b/src/Bridge/OpenRouter/PlatformFactory.php @@ -16,7 +16,7 @@ public static function create( ?HttpClientInterface $httpClient = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); - $handler = new Client($httpClient, $apiKey); + $handler = new Client($httpClient, $apiKey); return new Platform([$handler], [$handler]); }