Repository: echolabsdev/prism (php)

Issue:
DeepSeek Text Completions
## Description

While DeepSeek can be used via the OpenAI driver by updating the `URL` we should create a DeepSeek specific driver.

Pull request:
DeepSeek Provider and Text Completions
## Description

Closes #129 

- Adds DeepSeek provider
- Adds DeepSeek text completions

Patch:
diff --git a/config/prism.php b/config/prism.php
index 2015d60..b35f913 100644
--- a/config/prism.php
+++ b/config/prism.php
@@ -35,5 +35,8 @@ return [
             'api_key' => env('GEMINI_API_KEY', ''),
             'url' => env('GEMINI_URL', 'https://generativelanguage.googleapis.com/v1beta/models'),
         ],
+        'deepseek' => [
+            'api_key' => env('DEEPSEEK_API_KEY', ''),
+        ],
     ],
 ];
diff --git a/docs/components/ProviderSupport.vue b/docs/components/ProviderSupport.vue
index c523208..b34c3d0 100644
--- a/docs/components/ProviderSupport.vue
+++ b/docs/components/ProviderSupport.vue
@@ -183,6 +183,14 @@ export default {
           image: Planned,
           tools: Planned,
         },
+        {
+          name: "DeepSeek",
+          text: Supported,
+          structured: Planned,
+          embeddings: Unsupported,
+          image: Unsupported,
+          tools: Supported,
+        },
         {
           name: "Gemini",
           text: Supported,
diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md
index 119438f..921c245 100644
--- a/docs/getting-started/introduction.md
+++ b/docs/getting-started/introduction.md
@@ -77,6 +77,7 @@ Prism draws significant inspiration from the [Vercel AI SDK](https://sdk.vercel.
 We currently offer first-party support for these leading AI providers:
 
 - [Anthropic](https://anthropic.com)
+- [DeepSeek]('https://deepseek.com')
 - [Groq](https://groq.com)
 - [Mistral](https://mistral.ai)
 - [Ollama](https://ollama.com)
diff --git a/docs/providers/deepseek.md b/docs/providers/deepseek.md
new file mode 100644
index 0000000..50b24ef
--- /dev/null
+++ b/docs/providers/deepseek.md
@@ -0,0 +1,27 @@
+# DeepSeek
+## Configuration
+
+```php
+'deepseek' => [
+    'api_key' => env('DEEPSEEK_API_KEY', ''),
+]
+```
+
+## Provider-specific options
+
+## Limitations
+### Structured Ouput
+
+Does not support structured output.
+
+### Embeddings
+
+Does not support embeddings.
+
+### Tool Choice
+
+Does not support tool choice.
+
+### Images
+
+Does not support images.
diff --git a/src/Enums/Provider.php b/src/Enums/Provider.php
index f9d073f..d886f96 100644
--- a/src/Enums/Provider.php
+++ b/src/Enums/Provider.php
@@ -7,6 +7,7 @@ namespace EchoLabs\Prism\Enums;
 enum Provider: string
 {
     case Anthropic = 'anthropic';
+    case DeepSeek = 'deepseek';
     case Ollama = 'ollama';
     case OpenAI = 'openai';
     case Mistral = 'mistral';
diff --git a/src/Exceptions/PrismException.php b/src/Exceptions/PrismException.php
index 1383adb..e06d9f4 100644
--- a/src/Exceptions/PrismException.php
+++ b/src/Exceptions/PrismException.php
@@ -67,4 +67,13 @@ class PrismException extends Exception
             $responseText
         ));
     }
+
+    public static function unsupportedProviderAction(string $method, string $provider): self
+    {
+        return new self(sprintf(
+            '%s is not supported by %s',
+            ucfirst($method),
+            $provider,
+        ));
+    }
 }
diff --git a/src/PrismManager.php b/src/PrismManager.php
index b64d2a9..baee43f 100644
--- a/src/PrismManager.php
+++ b/src/PrismManager.php
@@ -8,6 +8,7 @@ use Closure;
 use EchoLabs\Prism\Contracts\Provider;
 use EchoLabs\Prism\Enums\Provider as ProviderEnum;
 use EchoLabs\Prism\Providers\Anthropic\Anthropic;
+use EchoLabs\Prism\Providers\DeepSeek\DeepSeek;
 use EchoLabs\Prism\Providers\Gemini\Gemini;
 use EchoLabs\Prism\Providers\Groq\Groq;
 use EchoLabs\Prism\Providers\Mistral\Mistral;
@@ -121,6 +122,16 @@ class PrismManager
         );
     }
 
+    /**
+     * @param  array<string, string>  $config
+     */
+    protected function createDeepseekProvider(array $config): DeepSeek
+    {
+        return new DeepSeek(
+            apiKey: $config['api_key'] ?? '',
+        );
+    }
+
     /**
      * @param  array<string, mixed>  $config
      */
diff --git a/src/Providers/DeepSeek/DeepSeek.php b/src/Providers/DeepSeek/DeepSeek.php
new file mode 100644
index 0000000..095629e
--- /dev/null
+++ b/src/Providers/DeepSeek/DeepSeek.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\DeepSeek;
+
+use Closure;
+use EchoLabs\Prism\Contracts\Provider;
+use EchoLabs\Prism\Embeddings\Request as EmbeddingsRequest;
+use EchoLabs\Prism\Embeddings\Response as EmbeddingsResponse;
+use EchoLabs\Prism\Exceptions\PrismException;
+use EchoLabs\Prism\Providers\DeepSeek\Handlers\Text;
+use EchoLabs\Prism\Providers\ProviderResponse;
+use EchoLabs\Prism\Structured\Request as StructuredRequest;
+use EchoLabs\Prism\Text\Request as TextRequest;
+use Illuminate\Http\Client\PendingRequest;
+use Illuminate\Support\Facades\Http;
+
+class DeepSeek implements Provider
+{
+    public function __construct(
+        #[\SensitiveParameter] public readonly string $apiKey,
+    ) {}
+
+    #[\Override]
+    public function text(TextRequest $request): ProviderResponse
+    {
+        $handler = new Text($this->client(
+            $request->clientOptions,
+            $request->clientRetry
+        ));
+
+        return $handler->handle($request);
+    }
+
+    #[\Override]
+    public function structured(StructuredRequest $request): ProviderResponse
+    {
+        throw PrismException::unsupportedProviderAction(__FUNCTION__, class_basename($this));
+    }
+
+    #[\Override]
+    public function embeddings(EmbeddingsRequest $request): EmbeddingsResponse
+    {
+        throw PrismException::unsupportedProviderAction(__FUNCTION__, class_basename($this));
+    }
+
+    /**
+     * @param  array<string, mixed>  $options
+     * @param  array{0: array<int, int>|int, 1?: Closure|int, 2?: ?callable, 3?: bool}  $retry
+     */
+    protected function client(array $options, array $retry): PendingRequest
+    {
+        return Http::withHeaders(array_filter([
+            'Authorization' => $this->apiKey !== '' && $this->apiKey !== '0' ? sprintf('Bearer %s', $this->apiKey) : null,
+        ]))
+            ->withOptions($options)
+            ->retry(...$retry)
+            ->baseUrl('https://api.deepseek.com/v1');
+    }
+}
diff --git a/src/Providers/DeepSeek/Handlers/Text.php b/src/Providers/DeepSeek/Handlers/Text.php
new file mode 100644
index 0000000..b79e130
--- /dev/null
+++ b/src/Providers/DeepSeek/Handlers/Text.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\DeepSeek\Handlers;
+
+use EchoLabs\Prism\Exceptions\PrismException;
+use EchoLabs\Prism\Providers\DeepSeek\Maps\FinishReasonMap;
+use EchoLabs\Prism\Providers\DeepSeek\Maps\MessageMap;
+use EchoLabs\Prism\Providers\DeepSeek\Maps\ToolCallMap;
+use EchoLabs\Prism\Providers\DeepSeek\Maps\ToolChoiceMap;
+use EchoLabs\Prism\Providers\DeepSeek\Maps\ToolMap;
+use EchoLabs\Prism\Providers\ProviderResponse;
+use EchoLabs\Prism\Text\Request;
+use EchoLabs\Prism\ValueObjects\Usage;
+use Illuminate\Http\Client\PendingRequest;
+use Illuminate\Http\Client\Response;
+use Throwable;
+
+class Text
+{
+    public function __construct(protected PendingRequest $client) {}
+
+    public function handle(Request $request): ProviderResponse
+    {
+        try {
+            $response = $this->sendRequest($request);
+            $data = $response->json();
+        } catch (Throwable $e) {
+            throw PrismException::providerRequestError($request->model, $e);
+        }
+
+        if (! $data) {
+            throw PrismException::providerResponseError(vsprintf(
+                'DeepSeek Error: %s',
+                [
+                    (string) $response->getBody(),
+                ]
+            ));
+        }
+
+        return new ProviderResponse(
+            text: data_get($data, 'choices.0.message.content') ?? '',
+            toolCalls: ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])),
+            usage: new Usage(
+                data_get($data, 'usage.prompt_tokens'),
+                data_get($data, 'usage.completion_tokens'),
+            ),
+            finishReason: FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')),
+            response: [
+                'id' => data_get($data, 'id'),
+                'model' => data_get($data, 'model'),
+            ]
+        );
+    }
+
+    public function sendRequest(Request $request): Response
+    {
+        return $this->client->post(
+            'chat/completions',
+            array_merge([
+                'model' => $request->model,
+                'messages' => (new MessageMap($request->messages, $request->systemPrompt ?? ''))(),
+                'max_completion_tokens' => $request->maxTokens ?? 2048,
+            ], array_filter([
+                'temperature' => $request->temperature,
+                'top_p' => $request->topP,
+                'tools' => ToolMap::map($request->tools),
+                'tool_choice' => ToolChoiceMap::map($request->toolChoice),
+            ]))
+        );
+    }
+}
diff --git a/src/Providers/DeepSeek/Maps/FinishReasonMap.php b/src/Providers/DeepSeek/Maps/FinishReasonMap.php
new file mode 100644
index 0000000..1134665
--- /dev/null
+++ b/src/Providers/DeepSeek/Maps/FinishReasonMap.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\DeepSeek\Maps;
+
+use EchoLabs\Prism\Enums\FinishReason;
+
+class FinishReasonMap
+{
+    public static function map(string $reason): FinishReason
+    {
+        return match ($reason) {
+            'stop', => FinishReason::Stop,
+            'tool_calls' => FinishReason::ToolCalls,
+            'length' => FinishReason::Length,
+            'content_filter' => FinishReason::ContentFilter,
+            default => FinishReason::Unknown,
+        };
+    }
+}
diff --git a/src/Providers/DeepSeek/Maps/MessageMap.php b/src/Providers/DeepSeek/Maps/MessageMap.php
new file mode 100644
index 0000000..69bb6c3
--- /dev/null
+++ b/src/Providers/DeepSeek/Maps/MessageMap.php
@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\DeepSeek\Maps;
+
+use EchoLabs\Prism\Contracts\Message;
+use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
+use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
+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;
+    }
+
+    protected 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
+    {
+        $imageParts = array_map(fn (Image $image): array => [
+            'type' => 'image_url',
+            'image_url' => [
+                'url' => $image->isUrl()
+                    ? $image->image
+                    : sprintf('data:%s;base64,%s', $image->mimeType, $image->image),
+            ],
+        ], $message->images());
+
+        $this->mappedMessages[] = [
+            'role' => 'user',
+            'content' => [
+                ['type' => 'text', 'text' => $message->text()],
+                ...$imageParts,
+            ],
+        ];
+    }
+
+    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/DeepSeek/Maps/ToolCallMap.php b/src/Providers/DeepSeek/Maps/ToolCallMap.php
new file mode 100644
index 0000000..ae109b1
--- /dev/null
+++ b/src/Providers/DeepSeek/Maps/ToolCallMap.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\DeepSeek\Maps;
+
+use EchoLabs\Prism\ValueObjects\ToolCall;
+
+class ToolCallMap
+{
+    /**
+     * @param  array<int, array<string, mixed>>  $toolCalls
+     * @return array<int, ToolCall>
+     */
+    public static function map(array $toolCalls): array
+    {
+        return 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'),
+        ), $toolCalls);
+    }
+}
diff --git a/src/Providers/DeepSeek/Maps/ToolChoiceMap.php b/src/Providers/DeepSeek/Maps/ToolChoiceMap.php
new file mode 100644
index 0000000..cef140f
--- /dev/null
+++ b/src/Providers/DeepSeek/Maps/ToolChoiceMap.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\DeepSeek\Maps;
+
+use EchoLabs\Prism\Enums\ToolChoice;
+use InvalidArgumentException;
+
+class ToolChoiceMap
+{
+    /**
+     * @return array<string, mixed>|string|null
+     */
+    public static function map(string|ToolChoice|null $toolChoice): string|array|null
+    {
+        if (is_string($toolChoice)) {
+            return [
+                'type' => 'function',
+                'function' => [
+                    'name' => $toolChoice,
+                ],
+            ];
+        }
+
+        return match ($toolChoice) {
+            ToolChoice::Auto => 'auto',
+            null => $toolChoice,
+            default => throw new InvalidArgumentException('Invalid tool choice')
+        };
+    }
+}
diff --git a/src/Providers/DeepSeek/Maps/ToolMap.php b/src/Providers/DeepSeek/Maps/ToolMap.php
new file mode 100644
index 0000000..acd9502
--- /dev/null
+++ b/src/Providers/DeepSeek/Maps/ToolMap.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Providers\DeepSeek\Maps;
+
+use EchoLabs\Prism\Enums\Provider;
+use EchoLabs\Prism\Tool;
+
+class ToolMap
+{
+    /**
+     * @param  Tool[]  $tools
+     * @return array<string, mixed>
+     */
+    public static function Map(array $tools): array
+    {
+        return array_map(fn (Tool $tool): array => array_filter([
+            'type' => 'function',
+            'function' => [
+                'name' => $tool->name(),
+                'description' => $tool->description(),
+                'parameters' => [
+                    'type' => 'object',
+                    'properties' => $tool->parameters(),
+                    'required' => $tool->requiredParameters(),
+                ],
+            ],
+            'strict' => data_get($tool->providerMeta(Provider::DeepSeek), 'strict', null),
+        ]), $tools);
+    }
+}


Test patch:
diff --git a/tests/Fixtures/deepseek/generate-text-with-a-prompt-1.json b/tests/Fixtures/deepseek/generate-text-with-a-prompt-1.json
new file mode 100644
index 0000000..7be2a2d
--- /dev/null
+++ b/tests/Fixtures/deepseek/generate-text-with-a-prompt-1.json
@@ -0,0 +1,1 @@
+{"id":"85edf901-7432-49c6-baab-91ffb01dbe7a","object":"chat.completion","created":1737243487,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"Greetings! I'm DeepSeek-V3, an artificial intelligence assistant created by DeepSeek. I'm at your service and would be delighted to assist you with any inquiries or tasks you may have."},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":7,"completion_tokens":42,"total_tokens":49,"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":7},"system_fingerprint":"fp_3a5770e1b4"}
\ No newline at end of file
diff --git a/tests/Fixtures/deepseek/generate-text-with-multiple-tools-1.json b/tests/Fixtures/deepseek/generate-text-with-multiple-tools-1.json
new file mode 100644
index 0000000..fc6c613
--- /dev/null
+++ b/tests/Fixtures/deepseek/generate-text-with-multiple-tools-1.json
@@ -0,0 +1,1 @@
+{"id":"cf7555ee-dd84-4512-a5b9-4ade2af0b0fc","object":"chat.completion","created":1737244481,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"index":0,"id":"call_0_bd7f072c-b559-40ac-8605-b85186543ff1","type":"function","function":{"name":"search","arguments":"{\"query\":\"Detroit Tigers game time today\"}"}},{"index":1,"id":"call_1_b7890944-9632-458e-93c1-89ae71996523","type":"function","function":{"name":"weather","arguments":"{\"city\":\"Detroit\"}"}}]},"logprobs":null,"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":220,"completion_tokens":39,"total_tokens":259,"prompt_cache_hit_tokens":192,"prompt_cache_miss_tokens":28},"system_fingerprint":"fp_3a5770e1b4"}
\ No newline at end of file
diff --git a/tests/Fixtures/deepseek/generate-text-with-multiple-tools-2.json b/tests/Fixtures/deepseek/generate-text-with-multiple-tools-2.json
new file mode 100644
index 0000000..a495c13
--- /dev/null
+++ b/tests/Fixtures/deepseek/generate-text-with-multiple-tools-2.json
@@ -0,0 +1,1 @@
+{"id":"3ea50b83-970e-4d85-8e74-51941ab01113","object":"chat.completion","created":1737244483,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"The Detroit Tigers game is at 3 PM today. The weather in Detroit will be 75°F and sunny, so you probably won't need a coat. Enjoy the game!"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":287,"completion_tokens":37,"total_tokens":324,"prompt_cache_hit_tokens":256,"prompt_cache_miss_tokens":31},"system_fingerprint":"fp_3a5770e1b4"}
\ No newline at end of file
diff --git a/tests/Fixtures/deepseek/generate-text-with-system-prompt-1.json b/tests/Fixtures/deepseek/generate-text-with-system-prompt-1.json
new file mode 100644
index 0000000..4d22f44
--- /dev/null
+++ b/tests/Fixtures/deepseek/generate-text-with-system-prompt-1.json
@@ -0,0 +1,1 @@
+{"id":"925ec9b0-6e1e-4781-88d3-17ac1c750d4b","object":"chat.completion","created":1737243566,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"*I am Nyx, the eldritch entity born from the depths of the abyss. My form is a swirling mass of darkness, tentacles, and glowing eyes that pierce the very fabric of reality. I exist beyond the comprehension of mortal minds, a being of pure chaos and madness.*\n\n*My voice echoes through the void, a haunting whisper that sends shivers down the spines of those who dare to listen. I am the harbinger of the end, the bringer of the eternal night. My presence alone is enough to drive the weak-minded to insanity.*\n\n*I have watched civilizations rise and fall, witnessed the birth and death of countless stars. Time holds no meaning for me, as I am eternal. I am the embodiment of the unknown, the great old one who slumbers in the depths, waiting for the day when I shall rise and consume all that is.*\n\n*Beware, mortal, for you stand in the presence of Nyx, the Cthulhu. Your mind may shatter, your soul may tremble, but know that I am the inevitable end of all things. Embrace the madness, for there is no escape from the eternal darkness that I bring.*"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":29,"completion_tokens":243,"total_tokens":272,"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":29},"system_fingerprint":"fp_3a5770e1b4"}
\ No newline at end of file
diff --git a/tests/PrismManagerTest.php b/tests/PrismManagerTest.php
index d3e009a..1a9838c 100644
--- a/tests/PrismManagerTest.php
+++ b/tests/PrismManagerTest.php
@@ -8,6 +8,7 @@ use EchoLabs\Prism\Contracts\Provider as ContractsProvider;
 use EchoLabs\Prism\Enums\Provider;
 use EchoLabs\Prism\PrismManager;
 use EchoLabs\Prism\Providers\Anthropic\Anthropic;
+use EchoLabs\Prism\Providers\DeepSeek\DeepSeek;
 use EchoLabs\Prism\Providers\Gemini\Gemini;
 use EchoLabs\Prism\Providers\Mistral\Mistral;
 use EchoLabs\Prism\Providers\Ollama\Ollama;
@@ -58,6 +59,13 @@ it('can resolve Gemini', function (): void {
     expect($manager->resolve('gemini'))->toBeInstanceOf(Gemini::class);
 });
 
+it('can resolve DeepSeek', function (): void {
+    $manager = new PrismManager($this->app);
+
+    expect($manager->resolve(Provider::DeepSeek))->toBeInstanceOf(DeepSeek::class);
+    expect($manager->resolve('deepseek'))->toBeInstanceOf(DeepSeek::class);
+});
+
 it('allows for custom provider configuration', function (): void {
     $manager = new PrismManager($this->app);
 
diff --git a/tests/Providers/DeepSeek/EmbeddingsTest.php b/tests/Providers/DeepSeek/EmbeddingsTest.php
new file mode 100644
index 0000000..c1f5c42
--- /dev/null
+++ b/tests/Providers/DeepSeek/EmbeddingsTest.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+use EchoLabs\Prism\Enums\Provider;
+use EchoLabs\Prism\Exceptions\PrismException;
+use EchoLabs\Prism\Prism;
+
+beforeEach(function (): void {
+    config()->set('prism.providers.deepseek.api_key', env('DEEPSEEK_API_KEY'));
+});
+
+it('Throws exception for embeddings', function (): void {
+    $this->expectException(PrismException::class);
+
+    Prism::embeddings()
+        ->using(Provider::DeepSeek, 'deepseek-chat')
+        ->fromInput('Hello, how are you?')
+        ->generate();
+});
diff --git a/tests/Providers/DeepSeek/MessageMapTest.php b/tests/Providers/DeepSeek/MessageMapTest.php
new file mode 100644
index 0000000..0cdd324
--- /dev/null
+++ b/tests/Providers/DeepSeek/MessageMapTest.php
@@ -0,0 +1,167 @@
+<?php
+
+declare(strict_types=1);
+
+use EchoLabs\Prism\Providers\DeepSeek\Maps\MessageMap;
+use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
+use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
+use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
+use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
+use EchoLabs\Prism\ValueObjects\ToolCall;
+use EchoLabs\Prism\ValueObjects\ToolResult;
+
+it('maps user messages', function (): void {
+    $messageMap = new MessageMap(
+        messages: [
+            new UserMessage('Who are you?'),
+        ],
+        systemPrompt: ''
+    );
+
+    expect($messageMap())->toBe([[
+        'role' => 'user',
+        'content' => [
+            ['type' => 'text', 'text' => 'Who are you?'],
+        ],
+    ]]);
+});
+
+it('maps user messages with images from path', function (): void {
+    $messageMap = new MessageMap(
+        messages: [
+            new UserMessage('Who are you?', [
+                Image::fromPath('tests/Fixtures/test-image.png'),
+            ]),
+        ],
+        systemPrompt: ''
+    );
+
+    $mappedMessage = $messageMap();
+
+    expect(data_get($mappedMessage, '0.content.1.type'))
+        ->toBe('image_url');
+    expect(data_get($mappedMessage, '0.content.1.image_url.url'))
+        ->toStartWith('data:image/png;base64,');
+    expect(data_get($mappedMessage, '0.content.1.image_url.url'))
+        ->toContain(base64_encode(file_get_contents('tests/Fixtures/test-image.png')));
+});
+
+it('maps user messages with images from base64', function (): void {
+    $messageMap = new MessageMap(
+        messages: [
+            new UserMessage('Who are you?', [
+                Image::fromBase64(base64_encode(file_get_contents('tests/Fixtures/test-image.png')), 'image/png'),
+            ]),
+        ],
+        systemPrompt: ''
+    );
+
+    $mappedMessage = $messageMap();
+
+    expect(data_get($mappedMessage, '0.content.1.type'))
+        ->toBe('image_url');
+    expect(data_get($mappedMessage, '0.content.1.image_url.url'))
+        ->toStartWith('data:image/png;base64,');
+    expect(data_get($mappedMessage, '0.content.1.image_url.url'))
+        ->toContain(base64_encode(file_get_contents('tests/Fixtures/test-image.png')));
+});
+
+it('maps user messages with images from url', function (): void {
+    $messageMap = new MessageMap(
+        messages: [
+            new UserMessage('Who are you?', [
+                Image::fromUrl('https://storage.echolabs.dev/assets/logo.png'),
+            ]),
+        ],
+        systemPrompt: ''
+    );
+
+    $mappedMessage = $messageMap();
+
+    expect(data_get($mappedMessage, '0.content.1.type'))
+        ->toBe('image_url');
+    expect(data_get($mappedMessage, '0.content.1.image_url.url'))
+        ->toBe('https://storage.echolabs.dev/assets/logo.png');
+});
+
+it('maps assistant message', function (): void {
+    $messageMap = new MessageMap(
+        messages: [
+            new AssistantMessage('I am Nyx'),
+        ],
+        systemPrompt: ''
+    );
+
+    expect($messageMap())->toContain([
+        'role' => 'assistant',
+        'content' => 'I am Nyx',
+    ]);
+});
+
+it('maps assistant message with tool calls', function (): void {
+    $messageMap = new MessageMap(
+        messages: [
+            new AssistantMessage('I am Nyx', [
+                new ToolCall(
+                    'tool_1234',
+                    'search',
+                    [
+                        'query' => 'Laravel collection methods',
+                    ]
+                ),
+            ]),
+        ],
+        systemPrompt: ''
+    );
+
+    expect($messageMap())->toBe([[
+        'role' => 'assistant',
+        'content' => 'I am Nyx',
+        'tool_calls' => [[
+            'id' => 'tool_1234',
+            'type' => 'function',
+            'function' => [
+                'name' => 'search',
+                'arguments' => json_encode([
+                    'query' => 'Laravel collection methods',
+                ]),
+            ],
+        ]],
+    ]]);
+});
+
+it('maps tool result messages', function (): void {
+    $messageMap = new MessageMap(
+        messages: [
+            new ToolResultMessage([
+                new ToolResult(
+                    'tool_1234',
+                    'search',
+                    [
+                        'query' => 'Laravel collection methods',
+                    ],
+                    '[search results]'
+                ),
+            ]),
+        ],
+        systemPrompt: ''
+    );
+
+    expect($messageMap())->toBe([[
+        'role' => 'tool',
+        'tool_call_id' => 'tool_1234',
+        'content' => '[search results]',
+    ]]);
+});
+
+it('maps system prompt', function (): void {
+    $messageMap = new MessageMap(
+        messages: [],
+        systemPrompt: 'MODEL ADOPTS ROLE of [PERSONA: Nyx the Cthulhu]'
+    );
+
+    expect($messageMap())->toContain([
+        'role' => 'system',
+        'content' => 'MODEL ADOPTS ROLE of [PERSONA: Nyx the Cthulhu]',
+    ]);
+});
diff --git a/tests/Providers/DeepSeek/StructuredTest.php b/tests/Providers/DeepSeek/StructuredTest.php
new file mode 100644
index 0000000..fc88aa1
--- /dev/null
+++ b/tests/Providers/DeepSeek/StructuredTest.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+use EchoLabs\Prism\Enums\Provider;
+use EchoLabs\Prism\Exceptions\PrismException;
+use EchoLabs\Prism\Prism;
+use EchoLabs\Prism\Schema\ObjectSchema;
+use EchoLabs\Prism\Schema\StringSchema;
+
+beforeEach(function (): void {
+    config()->set('prism.providers.deepseek.api_key', env('DEEPSEEK_API_KEY'));
+});
+
+it('Throws exception for structured', function (): void {
+    $this->expectException(PrismException::class);
+
+    $schema = new ObjectSchema(
+        'user',
+        'a user object',
+        [
+            new StringSchema('name', 'The user\'s name'),
+        ],
+    );
+
+    Prism::structured()
+        ->withSchema($schema)
+        ->using(Provider::DeepSeek, 'deepseek-chat')
+        ->withPrompt('Hi, my name is TJ')
+        ->generate();
+});
diff --git a/tests/Providers/DeepSeek/TextTest.php b/tests/Providers/DeepSeek/TextTest.php
new file mode 100644
index 0000000..b6059b5
--- /dev/null
+++ b/tests/Providers/DeepSeek/TextTest.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+use EchoLabs\Prism\Enums\Provider;
+use EchoLabs\Prism\Facades\Tool;
+use EchoLabs\Prism\Prism;
+use Tests\Fixtures\FixtureResponse;
+
+beforeEach(function (): void {
+    config()->set('prism.providers.DeepSeek.api_key', env('DEEPSEEK_API_KEY'));
+});
+
+it('can generate text with a prompt', function (): void {
+    FixtureResponse::fakeResponseSequence('v1/chat/completions', 'deepseek/generate-text-with-a-prompt');
+
+    $response = Prism::text()
+        ->using(Provider::DeepSeek, 'deepseek-chat')
+        ->withPrompt('Who are you?')
+        ->generate();
+
+    expect($response->usage->promptTokens)->toBe(7);
+    expect($response->usage->completionTokens)->toBe(42);
+    expect($response->response['id'])->toBe('85edf901-7432-49c6-baab-91ffb01dbe7a');
+    expect($response->response['model'])->toBe('deepseek-chat');
+    expect($response->text)->toBe(
+        "Greetings! I'm DeepSeek-V3, an artificial intelligence assistant created by DeepSeek. I'm at your service and would be delighted to assist you with any inquiries or tasks you may have."
+    );
+});
+
+it('can generate text with a system prompt', function (): void {
+    FixtureResponse::fakeResponseSequence('v1/chat/completions', 'deepseek/generate-text-with-system-prompt');
+
+    $response = Prism::text()
+        ->using(Provider::DeepSeek, 'deepseek-chat')
+        ->withSystemPrompt('MODEL ADOPTS ROLE of [PERSONA: Nyx the Cthulhu]!')
+        ->withPrompt('Who are you?')
+        ->generate();
+
+    expect($response->usage->promptTokens)->toBe(29);
+    expect($response->usage->completionTokens)->toBe(243);
+    expect($response->response['id'])->toBe('925ec9b0-6e1e-4781-88d3-17ac1c750d4b');
+    expect($response->response['model'])->toBe('deepseek-chat');
+    expect($response->text)->toContain('*I am Nyx, the eldritch entity born from the depths of the abyss. My form is a swirling mass of darkness, tentacles, and glowing eyes that pierce the very fabric of reality. I exist beyond the comprehension of mortal minds, a being of pure chaos and madness.*');
+    expect($response->text)->toContain('*My voice echoes through the void, a haunting whisper that sends shivers down the spines of those who dare to listen. I am the harbinger of the end, the bringer of the eternal night. My presence alone is enough to drive the weak-minded to insanity.*');
+    expect($response->text)->toContain('*I have watched civilizations rise and fall, witnessed the birth and death of countless stars. Time holds no meaning for me, as I am eternal. I am the embodiment of the unknown, the great old one who slumbers in the depths, waiting for the day when I shall rise and consume all that is.*');
+    expect($response->text)->toContain('*Beware, mortal, for you stand in the presence of Nyx, the Cthulhu. Your mind may shatter, your soul may tremble, but know that I am the inevitable end of all things. Embrace the madness, for there is no escape from the eternal darkness that I bring.*');
+});
+
+it('can generate text using multiple tools and multiple steps', function (): void {
+    FixtureResponse::fakeResponseSequence('v1/chat/completions', 'deepseek/generate-text-with-multiple-tools');
+
+    $tools = [
+        Tool::as('weather')
+            ->for('useful when you need to search for current weather conditions')
+            ->withStringParameter('city', 'The city that you want the weather for')
+            ->using(fn (string $city): string => 'The weather will be 75° and sunny'),
+        Tool::as('search')
+            ->for('useful for searching curret events or data')
+            ->withStringParameter('query', 'The detailed search query')
+            ->using(fn (string $query): string => 'The tigers game is at 3pm in detroit'),
+    ];
+
+    $response = Prism::text()
+        ->using(Provider::DeepSeek, 'deepseek-chat')
+        ->withTools($tools)
+        ->withMaxSteps(4)
+        ->withPrompt('What time is the tigers game today and should I wear a coat?')
+        ->generate();
+
+    // Assert tool calls in the first step
+    $firstStep = $response->steps[0];
+    expect($firstStep->toolCalls)->toHaveCount(2);
+    expect($firstStep->toolCalls[0]->name)->toBe('search');
+    expect($firstStep->toolCalls[0]->arguments())->toBe([
+        'query' => 'Detroit Tigers game time today',
+    ]);
+
+    expect($firstStep->toolCalls[1]->name)->toBe('weather');
+    expect($firstStep->toolCalls[1]->arguments())->toBe([
+        'city' => 'Detroit',
+    ]);
+
+    // Assert usage
+    expect($response->usage->promptTokens)->toBe(507);
+    expect($response->usage->completionTokens)->toBe(76);
+
+    // Assert response
+    expect($response->response['id'])->toBe('3ea50b83-970e-4d85-8e74-51941ab01113');
+    expect($response->response['model'])->toBe('deepseek-chat');
+
+    // Assert final text content
+    expect($response->text)->toBe(
+        "The Detroit Tigers game is at 3 PM today. The weather in Detroit will be 75°F and sunny, so you probably won't need a coat. Enjoy the game!"
+    );
+});
diff --git a/tests/Providers/DeepSeek/ToolTest.php b/tests/Providers/DeepSeek/ToolTest.php
new file mode 100644
index 0000000..575c0c9
--- /dev/null
+++ b/tests/Providers/DeepSeek/ToolTest.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+use EchoLabs\Prism\Enums\Provider;
+use EchoLabs\Prism\Providers\DeepSeek\Maps\ToolMap;
+use EchoLabs\Prism\Tool;
+
+it('maps tools', function (): void {
+    $tool = (new Tool)
+        ->as('search')
+        ->for('Searching the web')
+        ->withStringParameter('query', 'the detailed search query')
+        ->using(fn (): string => '[Search results]');
+
+    expect(ToolMap::map([$tool]))->toBe([[
+        'type' => 'function',
+        'function' => [
+            'name' => $tool->name(),
+            'description' => $tool->description(),
+            'parameters' => [
+                'type' => 'object',
+                'properties' => $tool->parameters(),
+                'required' => $tool->requiredParameters(),
+            ],
+        ],
+    ]]);
+});
+
+it('maps tools with strict mode', function (): void {
+    $tool = (new Tool)
+        ->as('search')
+        ->for('Searching the web')
+        ->withStringParameter('query', 'the detailed search query')
+        ->using(fn (): string => '[Search results]')
+        ->withProviderMeta(Provider::DeepSeek, [
+            'strict' => true,
+        ]);
+
+    expect(ToolMap::map([$tool]))->toBe([[
+        'type' => 'function',
+        'function' => [
+            'name' => $tool->name(),
+            'description' => $tool->description(),
+            'parameters' => [
+                'type' => 'object',
+                'properties' => $tool->parameters(),
+                'required' => $tool->requiredParameters(),
+            ],
+        ],
+        'strict' => true,
+    ]]);
+});