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..4d349c8e --- /dev/null +++ b/src/Bridge/OpenRouter/Client.php @@ -0,0 +1,62 @@ +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..65d32e59 --- /dev/null +++ b/src/Bridge/OpenRouter/GenericModel.php @@ -0,0 +1,55 @@ + $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..da36ebbc --- /dev/null +++ b/src/Bridge/OpenRouter/PlatformFactory.php @@ -0,0 +1,23 @@ +