Repository: prism-php/prism (PHP)

Issue:
Mistral Support
## Description

Add Mistral to the first party supported providers. We should do some investigation but I'm pretty sure all we need to do is add the configuration using the OpenAI driver, similar to Ollama.

https://docs.mistral.ai/api/


Discussion: Can I work on this @sixlive ?
@pushpak1300 yeah! Go for it!



Fix (patch):
diff --git a/config/prism.php b/config/prism.php
index 3bf739257..ac2b0ded5 100644
--- a/config/prism.php
+++ b/config/prism.php
@@ -17,5 +17,9 @@
         'ollama' => [
             'url' => env('OLLAMA_URL', 'http://localhost:11434/v1'),
         ],
+        'mistral' => [
+            'api_key' => env('MISTRAL_API_KEY', ''),
+            'url' => env('MISTRAL_URL', 'https://api.mistral.ai/v1'),
+        ],
     ],
 ];
diff --git a/src/Enums/Provider.php b/src/Enums/Provider.php
index b3bed78ae..17ae81b1e 100644
--- a/src/Enums/Provider.php
+++ b/src/Enums/Provider.php
@@ -9,4 +9,5 @@ enum Provider: string
     case Anthropic = 'anthropic';
     case Ollama = 'ollama';
     case OpenAI = 'openai';
+    case Mistral = 'mistral';
 }
diff --git a/src/PrismManager.php b/src/PrismManager.php
index 18c105224..20dee46fa 100644
--- a/src/PrismManager.php
+++ b/src/PrismManager.php
@@ -8,6 +8,7 @@
 use EchoLabs\Prism\Contracts\Provider;
 use EchoLabs\Prism\Enums\Provider as ProviderEnum;
 use EchoLabs\Prism\Providers\Anthropic\Anthropic;
+use EchoLabs\Prism\Providers\Mistral\Mistral;
 use EchoLabs\Prism\Providers\Ollama\Ollama;
 use EchoLabs\Prism\Providers\OpenAI\OpenAI;
 use Illuminate\Contracts\Foundation\Application;
@@ -81,6 +82,17 @@ protected function createOllamaProvider(array $config): Ollama
         );
     }
 
+    /**
+     * @param  array<string, string>  $config
+     */
+    protected function createMistralProvider(array $config): Mistral
+    {
+        return new Mistral(
+            apiKey: $config['api_key'],
+            url: $config['url'],
+        );
+    }
+
     /**
      * @param  array<string, string>  $config
      */
diff --git a/src/Providers/Mistral/Client.php b/src/Providers/Mistral/Client.php
new file mode 100644
index 000000000..ae1cda8bb
--- /dev/null
+++ b/src/Providers/Mistral/Client.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\Mistral;
+
+use Illuminate\Http\Client\PendingRequest;
+use Illuminate\Http\Client\Response;
+use Illuminate\Support\Facades\Http;
+
+class Client
+{
+    protected PendingRequest $client;
+
+    public function __construct(
+        public readonly string $url,
+        public readonly string $apiKey,
+    ) {
+        $this->client = Http::withHeaders(array_filter([
+            'Authorization' => sprintf('Bearer %s', $this->apiKey),
+        ]))->baseUrl($this->url);
+    }
+
+    /**
+     * @param  array<int, mixed>  $messages
+     * @param  array<int, mixed>|null  $tools
+     */
+    public function messages(
+        string $model,
+        array $messages,
+        ?int $maxTokens,
+        int|float|null $temperature,
+        int|float|null $topP,
+        ?array $tools,
+    ): Response {
+        return $this->client->post(
+            'chat/completions',
+            array_merge([
+                'model' => $model,
+                'messages' => $messages,
+                'max_tokens' => $maxTokens ?? 2048,
+            ], array_filter([
+                'temperature' => $temperature,
+                'top_p' => $topP,
+                'tools' => $tools,
+            ]))
+        );
+    }
+}
diff --git a/src/Providers/Mistral/MessageMap.php b/src/Providers/Mistral/MessageMap.php
new file mode 100644
index 000000000..bf8732fdb
--- /dev/null
+++ b/src/Providers/Mistral/MessageMap.php
@@ -0,0 +1,103 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\Mistral;
+
+use EchoLabs\Prism\Contracts\Message;
+use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
+use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
+use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
+use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
+use EchoLabs\Prism\ValueObjects\ToolCall;
+use Exception;
+
+class MessageMap
+{
+    /** @var array<int, mixed> */
+    protected $mappedMessages = [];
+
+    /**
+     * @param  array<int, Message>  $messages
+     */
+    public function __construct(
+        protected array $messages,
+        protected string $systemPrompt
+    ) {
+        if ($systemPrompt !== '' && $systemPrompt !== '0') {
+            $this->messages = array_merge(
+                [new SystemMessage($systemPrompt)],
+                $this->messages
+            );
+        }
+    }
+
+    /**
+     * @return array<int, mixed>
+     */
+    public function __invoke(): array
+    {
+        array_map(
+            fn (Message $message) => $this->mapMessage($message),
+            $this->messages
+        );
+
+        return $this->mappedMessages;
+    }
+
+    public function mapMessage(Message $message): void
+    {
+        match ($message::class) {
+            UserMessage::class => $this->mapUserMessage($message),
+            AssistantMessage::class => $this->mapAssistantMessage($message),
+            ToolResultMessage::class => $this->mapToolResultMessage($message),
+            SystemMessage::class => $this->mapSystemMessage($message),
+            default => throw new Exception('Could not map message type '.$message::class),
+        };
+    }
+
+    protected function mapSystemMessage(SystemMessage $message): void
+    {
+        $this->mappedMessages[] = [
+            'role' => 'system',
+            'content' => $message->content(),
+        ];
+    }
+
+    protected function mapToolResultMessage(ToolResultMessage $message): void
+    {
+        foreach ($message->toolResults as $toolResult) {
+            $this->mappedMessages[] = [
+                'role' => 'tool',
+                'tool_call_id' => $toolResult->toolCallId,
+                'content' => $toolResult->result,
+            ];
+        }
+    }
+
+    protected function mapUserMessage(UserMessage $message): void
+    {
+        $this->mappedMessages[] = [
+            'role' => 'user',
+            'content' => $message->content(),
+        ];
+    }
+
+    protected function mapAssistantMessage(AssistantMessage $message): void
+    {
+        $toolCalls = array_map(fn (ToolCall $toolCall): array => [
+            'id' => $toolCall->id,
+            'type' => 'function',
+            'function' => [
+                'name' => $toolCall->name,
+                'arguments' => json_encode($toolCall->arguments()),
+            ],
+        ], $message->toolCalls());
+
+        $this->mappedMessages[] = array_filter([
+            'role' => 'assistant',
+            'content' => $message->content(),
+            'tool_calls' => $toolCalls,
+        ]);
+    }
+}
diff --git a/src/Providers/Mistral/Mistral.php b/src/Providers/Mistral/Mistral.php
new file mode 100644
index 000000000..550b8503e
--- /dev/null
+++ b/src/Providers/Mistral/Mistral.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\Mistral;
+
+use EchoLabs\Prism\Contracts\Provider;
+use EchoLabs\Prism\Enums\FinishReason;
+use EchoLabs\Prism\Exceptions\PrismException;
+use EchoLabs\Prism\Providers\ProviderResponse;
+use EchoLabs\Prism\Requests\TextRequest;
+use EchoLabs\Prism\ValueObjects\ToolCall;
+use EchoLabs\Prism\ValueObjects\Usage;
+use Throwable;
+
+class Mistral implements Provider
+{
+    protected Client $client;
+
+    protected string $model;
+
+    public function __construct(
+        public readonly string $url,
+        public readonly string $apiKey,
+    ) {
+        $this->client = new Client(
+            url: $this->url,
+            apiKey: $this->apiKey,
+        );
+    }
+
+    #[\Override]
+    public function usingModel(string $model): self
+    {
+        $this->model = $model;
+
+        return $this;
+    }
+
+    #[\Override]
+    public function text(TextRequest $request): ProviderResponse
+    {
+        try {
+            $response = $this->client->messages(
+                model: $this->model,
+                messages: (new MessageMap(
+                    $request->messages,
+                    $request->systemPrompt ?? '',
+                ))(),
+                maxTokens: $request->maxTokens,
+                temperature: $request->temperature,
+                topP: $request->topP,
+                tools: Tool::map($request->tools),
+            );
+        } catch (Throwable $e) {
+            throw PrismException::providerRequestError($this->model, $e);
+        }
+
+        $data = $response->json();
+
+        if (data_get($data, 'error') || ! $data) {
+            throw PrismException::providerResponseError(vsprintf(
+                'Mistral Error:  [%s] %s',
+                [
+                    data_get($data, 'error.type', 'unknown'),
+                    data_get($data, 'error.message', 'unknown'),
+                ]
+            ));
+        }
+
+        $toolCalls = array_map(fn (array $toolCall): ToolCall => new ToolCall(
+            id: data_get($toolCall, 'id'),
+            name: data_get($toolCall, 'function.name'),
+            arguments: data_get($toolCall, 'function.arguments'),
+        ), data_get($data, 'choices.0.message.tool_calls', []) ?? []);
+
+        return new ProviderResponse(
+            text: data_get($data, 'choices.0.message.content') ?? '',
+            toolCalls: $toolCalls,
+            usage: new Usage(
+                data_get($data, 'usage.prompt_tokens'),
+                data_get($data, 'usage.completion_tokens'),
+            ),
+            finishReason: $this->mapFinishReason(data_get($data, 'choices.0.finish_reason', '')),
+            response: [
+                'id' => data_get($data, 'id'),
+                'model' => data_get($data, 'model'),
+            ]
+        );
+    }
+
+    protected function mapFinishReason(string $stopReason): FinishReason
+    {
+        return match ($stopReason) {
+            'stop', => FinishReason::Stop,
+            'tool_calls' => FinishReason::ToolCalls,
+            'length' => FinishReason::Length,
+            'content_filter' => FinishReason::ContentFilter,
+            default => FinishReason::Unknown,
+        };
+    }
+}
diff --git a/src/Providers/Mistral/Tool.php b/src/Providers/Mistral/Tool.php
new file mode 100644
index 000000000..641b7b86f
--- /dev/null
+++ b/src/Providers/Mistral/Tool.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\Mistral;
+
+use EchoLabs\Prism\Providers\ProviderTool;
+use EchoLabs\Prism\Tool as PrismTool;
+
+class Tool extends ProviderTool
+{
+    #[\Override]
+    public static function toArray(PrismTool $tool): array
+    {
+        return [
+            'type' => 'function',
+            'function' => [
+                'name' => $tool->name(),
+                'description' => $tool->description(),
+                'parameters' => [
+                    'type' => 'object',
+                    'properties' => collect($tool->parameters())
+                        ->keyBy('name')
+                        ->map(fn (array $field): array => [
+                            'description' => $field['description'],
+                            'type' => $field['type'],
+                        ])
+                        ->toArray(),
+                    'required' => $tool->requiredParameters(),
+                ],
+            ],
+        ];
+    }
+}