diff --git a/.github/workflows/deepseek-automation-test.yml b/.github/workflows/deepseek-automation-test.yml
index 2b78312..6a10da8 100644
--- a/.github/workflows/deepseek-automation-test.yml
+++ b/.github/workflows/deepseek-automation-test.yml
@@ -18,7 +18,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
- php: ['8.1', '8.2', '8.3', '8.4']
+ php: ['8.2', '8.3', '8.4']
steps:
- name: Checkout code
diff --git a/.gitignore b/.gitignore
index b1df40d..9bb0c5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,13 +3,11 @@ vendor
tests/TestSupport/temp
tests/temp
composer.lock
-phpunit.xml
.env
.phpunit.cache/
.phpunit.result.cache
.phpunit.cache/test-results
.php-cs-fixer.cache
-phpstan.neon
tests/Support/temp/
.idea/
AGENTS.md
@@ -17,7 +15,6 @@ AGENTS.md
/.php-cs-fixer.cache
/.php-cs-fixer.php
/composer.lock
-/phpunit.xml
/vendor/
*.swp
*.swo
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9682fa..ca9dd7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,129 @@
# Changelog
-All notable changes to `deepseek-php-client` will be documented in this file
+All notable changes to `deepseek-php-client` are documented in this file.
-## 1.0.0 - 201X-XX-XX
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-- initial release
+---
+
+## Backward Compatibility Commitment
+
+This package is used in 100k+ production installs. We take backward compatibility seriously and follow semantic versioning strictly within the `v2.x` line.
+
+**Our commitments for the entire `v2.x` line:**
+
+- No public method, class, trait, enum case, or constant will be **removed** or **renamed**.
+- No public method's **return type** will change.
+- No public method's existing **parameters** will change type or be removed (new optional parameters with defaults may be added).
+- No published interface ([`ClientContract`](src/Contracts/ClientContract.php), `ResourceContract`, `ResultContract`, `ApiFactoryContract`) will gain new required methods. New methods on the client implementation will be exposed via separate, additive interfaces.
+- Default behavior of existing methods will not silently change in ways that affect cost, output, or correctness (e.g. raising default `max_tokens`).
+
+**What's coming in `v2.1.x`:** Every new DeepSeek API feature (V4 models, thinking mode, FIM completion, Anthropic format, user balance, `stop` / `top_p` / `tool_choice` / `logprobs` / `user_id`, chat prefix completion, etc.) will be delivered as **additive** new methods, optional parameters, and enum cases. Bug fixes (`/v3` base URL, ignored params in `chat()` / `code()`, keep-alive line stripping) are also in scope.
+
+**What's reserved for `v3.0.0`:** Removing deprecated `Models::CODER` / `Models::R1` / `Models::R1Zero`, removing the `Coder` class and `HasCoder` trait, raising default `max_tokens`, retyping `run()` to return a structured DTO, expanding `ClientContract` to match the implementation. All of these will be announced via `@deprecated` notices throughout `v2.x` and shipped with a complete migration guide in [MIGRATION.md](MIGRATION.md).
+
+See [TODO.md](TODO.md) for the full feature gap analysis with per-item BC classification.
+
+---
+
+## [Unreleased] - v2.2.0 (planned)
+
+> Additive only. Zero breaking changes from v2.x. See [TODO.md](TODO.md) for the source list.
+
+### Generation parameters and DX
+- Thinking mode setters: `setThinking(array $config)` and `setReasoningEffort(string $effort)`.
+- New sampling / generation parameter setters: `setStop()`, `setTopP()`, `setToolChoice()`, `setLogprobs()`, `setTopLogprobs()`, `setUserId()`.
+- Optional `?string $name` parameter on `query()` and `buildQuery()` for the OpenAI message `name` field.
+- `setSystemMessage(string $content)` convenience method.
+- Tool `strict` mode helper for function definitions.
+- Response introspection accessors on `SuccessResult`: `getMessage()`, `getUsage()`, `getReasoningContent()`, `getToolCalls()`, `getFinishReason()`, `getCacheHitTokens()` — `run()` continues to return `string`.
+- `getQueries()`, `getConfig()`, `reset()` introspection helpers.
+- `DefaultConfigs::MAX_TOKENS` and `DefaultConfigs::RESPONSE_FORMAT_TYPE` cases (`TemperatureValues::MAX_TOKENS` / `RESPONSE_FORMAT_TYPE` deprecated).
+- Additive `ExtendedClientContract` interface (preserves the existing `ClientContract`).
+
+## [Unreleased] - v2.3.0 (planned)
+
+- Real streaming via new `runStreamed(callable $onChunk): void` method (existing `->withStream()->run()` string-returning behavior preserved).
+- `stream_options.include_usage` exposure.
+- Chat Prefix Completion (Beta): `queryAssistantPrefix(string $content)` and `/beta` base URL opt-in.
+- `getUserBalance()` method, `EndpointSuffixes::USER_BALANCE` enum case, and `UserBalanceResult` DTO.
+- FIM Completion (Beta): new resource class for `POST /completions` (Fill-In-the-Middle).
+- Rate-limit handling: new `RateLimitResult` class with parsed `Retry-After` header for HTTP 429 responses.
+
+> Note: Anthropic API format support is intentionally not on the v2.x roadmap. It may be reintroduced in a later release if there is sufficient community demand.
+
+---
+
+## [2.1.0] - 2026-05-22
+
+> Foundation + Bug Fixes. Zero breaking changes from `v2.0.x`. All deprecated symbols remain fully functional throughout the `v2.x` line.
+
+### Added
+- New `Models::V4_PRO` (`deepseek-v4-pro`) and `Models::V4_FLASH` (`deepseek-v4-flash`) enum cases. Both models support DeepSeek's 1M-token context window and dual thinking / non-thinking modes (per the [V4 Preview announcement](https://api-docs.deepseek.com/news/news260424)).
+
+### Fixed
+- **Default `baseUrl` corrected** from `https://api.deepseek.com/v3` to `https://api.deepseek.com`. The `/v3` path was never a valid DeepSeek API endpoint. Users who passed an explicit `baseUrl` to `DeepSeekClient::build()` are unaffected.
+- **`chat()` and `code()` shortcuts** now honor `temperature`, `maxTokens`, `tools`, and `responseFormat` set on the client. Previously these settings were silently dropped from the request body when using the shortcut methods; the request body now matches what `run()` sends.
+- **Keep-Alive padding stripped from responses.** The DeepSeek API may send empty lines (non-streaming) or `: keep-alive` SSE comments (streaming) while waiting for inference to start. These are now removed before the response content is exposed to user code. See the [DeepSeek Rate Limit docs](https://api-docs.deepseek.com/quick_start/rate_limit#request-keep-alive-mechanism) for details.
+
+### Deprecated
+The following symbols are deprecated and will be removed in `v3.0.0`. They remain fully functional throughout the `v2.x` line — only IDE `@deprecated` notices are emitted (no `trigger_error()`).
+
+- `Models::CHAT` — the `deepseek-chat` alias retires from the DeepSeek API on 2026-07-24. Use `Models::V4_FLASH` (with `setThinking(['type' => 'disabled'])` in v2.2.0+ for non-thinking mode).
+- `Models::CODER` — `deepseek-coder` no longer exists in the DeepSeek API. Use `Models::V4_PRO` or `Models::V4_FLASH`.
+- `Models::R1` — the `DeepSeek-R1` alias retires from the DeepSeek API on 2026-07-24. Use `Models::V4_FLASH` (with `setThinking(['type' => 'enabled'])` in v2.2.0+ for thinking mode).
+- `Models::R1Zero` — `DeepSeek-R1-Zero` was never a valid DeepSeek API model id.
+
+### Documentation
+- README refreshed: default temperature corrected (1.3, not 0.8); `baseUrl` example updated; advanced-configuration example now uses `Models::V4_PRO`; `getModelsList()` example output updated to include V4 models; new "Supported Models" callout under Features.
+- `docs/FUNCTION-CALLING.md` updated: JSON examples use `deepseek-v4-pro`; new "Thinking-mode caveat" section explaining that `reasoning_content` must be echoed back on tool turns to avoid HTTP 400 from the API.
+
+### Internal
+- New test file: `tests/Feature/V210ChangesTest.php` covering V4 enum cases, default base URL, Keep-Alive stripping (both non-streaming and streaming), and `chat()` / `code()` request body completeness.
+
+---
+
+## [2.0.6] - 2025
+
+Current published baseline prior to v2.1.0. No breaking changes from `2.0.0`.
+
+Patch-level fixes and documentation updates rolled up since `2.0.0`. This release marks the point from which the [Backward Compatibility Commitment](#backward-compatibility-commitment) above applies.
+
+---
+
+## [2.0.0] - 2025-02-01
+
+### Changed (Breaking)
+- **Namespace renamed** from `DeepseekPhp` to `DeepSeek`. All imports in user code must be updated. See [MIGRATION.md](MIGRATION.md) for details.
+
+ Replace:
+ ```php
+ use DeepseekPhp\SomeClass;
+ ```
+ With:
+ ```php
+ use DeepSeek\SomeClass;
+ ```
+
+---
+
+## [Roadmap] - v3.0.0 (no ETA)
+
+> Breaking changes only. Will ship with a complete migration guide in [MIGRATION.md](MIGRATION.md). Users will get at least one full `v2.1.x` release with `@deprecated` notices before any of these land.
+
+### Removed
+- Deprecated `Models` enum cases: `CHAT`, `CODER`, `R1`, `R1Zero` (per the 2026-07-24 DeepSeek API retirement of the `deepseek-chat` and `deepseek-reasoner` aliases).
+- `Resources\Coder` class and `Traits\Resources\HasCoder` trait (including `code()`).
+- `Enums\Configs\TemperatureValues::MAX_TOKENS` and `RESPONSE_FORMAT_TYPE` cases.
+
+### Changed (Breaking)
+- `run()` will return a structured `ChatCompletionResult` DTO instead of a raw JSON `string`. The current string-returning behavior moves to a new method (`runRaw()` or equivalent).
+- Default `MAX_TOKENS` raised from 4096 to a V4-appropriate value (V4 models support 384K output tokens — current default is ~1% of capacity).
+- `ClientContract` expanded to declare all methods the implementation provides: `resetQueries()`, `setTemperature()`, `setMaxTokens()`, `setResponseFormat()`, `setResult()`, `getResult()`, and the 4-arg `build()` signature including `?string $clientType`.
+- Default `baseUrl` no longer accepts the legacy `/v3` suffix (already removed in `v2.1.x` as a fix; v3.0.0 removes the back-compat shim).
+
+---
+
+## [1.0.0] - 201X-XX-XX
+
+- Initial release.
diff --git a/README.md b/README.md
index 1209d00..8900581 100644
--- a/README.md
+++ b/README.md
@@ -46,11 +46,18 @@
- **Seamless API Integration**: PHP-first interface for DeepSeek's AI capabilities.
- **Fluent Builder Pattern**: Chainable methods for intuitive request building.
- **Enterprise Ready**: PSR-18 compliant HTTP client integration.
-- **Model Flexibility**: Support for multiple DeepSeek models (Coder, Chat, etc.).
+- **Latest DeepSeek V4 Models**: First-class support for `deepseek-v4-pro` and `deepseek-v4-flash` with 1M-token context windows and thinking / non-thinking modes.
- **Streaming Ready**: Built-in support for real-time response handling.
- **Many Http Clients**: easy to use `Guzzle http client` (default) , or `symfony http client`.
- **Framework Friendly**: Laravel & Symfony packages available.
+> **Supported Models**
+>
+> - `Models::V4_PRO` — flagship 1.6T/49B-active model, max 384K output tokens.
+> - `Models::V4_FLASH` — fast, economical 284B/13B-active model, max 384K output tokens.
+>
+> Legacy `Models::CHAT`, `Models::CODER`, `Models::R1`, and `Models::R1Zero` are deprecated and will be removed in v3.0.0. The `deepseek-chat` and `deepseek-reasoner` aliases retire from the DeepSeek API on **2026-07-24**.
+
---
## 📦 Installation
@@ -83,8 +90,10 @@ echo $response;
```
📌 Defaults used:
-- Model: `deepseek-chat`
-- Temperature: 0.8
+- Model: API default (no `model` field sent unless you call `withModel()`)
+- Temperature: 1.3 (`TemperatureValues::GENERAL_CONVERSATION`)
+- Max tokens: 4096
+- Response format: `text`
### Advanced Configuration
@@ -92,10 +101,10 @@ echo $response;
use DeepSeek\DeepSeekClient;
use DeepSeek\Enums\Models;
-$client = DeepSeekClient::build(apiKey:'your-api-key', baseUrl:'https://api.deepseek.com/v3', timeout:30, clientType:'guzzle');
+$client = DeepSeekClient::build(apiKey:'your-api-key', baseUrl:'https://api.deepseek.com', timeout:30, clientType:'guzzle');
$response = $client
- ->withModel(Models::CODER->value)
+ ->withModel(Models::V4_PRO->value)
->withStream()
->setTemperature(1.2)
->setMaxTokens(8192)
@@ -149,7 +158,7 @@ ex with symfony:
// with defaults baseUrl and timeout
$client = DeepSeekClient::build('your-api-key', clientType:'symfony')
// with customization
-$client = DeepSeekClient::build(apiKey:'your-api-key', baseUrl:'https://api.deepseek.com/v3', timeout:30, clientType:'symfony');
+$client = DeepSeekClient::build(apiKey:'your-api-key', baseUrl:'https://api.deepseek.com', timeout:30, clientType:'symfony');
$client->query('Explain quantum computing in simple terms')
->run();
@@ -164,7 +173,16 @@ $response = DeepSeekClient::build('your-api-key')
->getModelsList()
->run();
-echo $response; // {"object":"list","data":[{"id":"deepseek-chat","object":"model","owned_by":"deepseek"},{"id":"deepseek-reasoner","object":"model","owned_by":"deepseek"}]}
+echo $response;
+// {
+// "object": "list",
+// "data": [
+// {"id": "deepseek-v4-pro", "object": "model", "owned_by": "deepseek"},
+// {"id": "deepseek-v4-flash", "object": "model", "owned_by": "deepseek"},
+// {"id": "deepseek-chat", "object": "model", "owned_by": "deepseek"}, // deprecated, retires 2026-07-24
+// {"id": "deepseek-reasoner", "object": "model", "owned_by": "deepseek"} // deprecated, retires 2026-07-24
+// ]
+// }
```
diff --git a/composer.json b/composer.json
index e9e0a67..626c9e4 100644
--- a/composer.json
+++ b/composer.json
@@ -49,9 +49,9 @@
"role": "creator"
}
],
- "version": "2.0.6",
+ "version": "2.1.0",
"require": {
- "php": "^8.1.0",
+ "php": "^8.2.0",
"nyholm/psr7": "^1.8",
"php-http/discovery": "^1.20.0",
"php-http/multipart-stream-builder": "^1.4.2",
diff --git a/docs/FUNCTION-CALLING.md b/docs/FUNCTION-CALLING.md
index 4d115cf..268ac08 100644
--- a/docs/FUNCTION-CALLING.md
+++ b/docs/FUNCTION-CALLING.md
@@ -53,7 +53,7 @@ Output response like.
"id": "chat_12345",
"object": "chat.completion",
"created": 1677654321,
- "model": "deepseek-chat",
+ "model": "deepseek-v4-pro",
"choices": [
{
"index": 0,
@@ -136,7 +136,7 @@ Request like
"content": "{\"temperature\":22,\"condition\":\"Sunny\"}"
}
],
- "model": "deepseek-chat",
+ "model": "deepseek-v4-pro",
"stream": false,
"temperature": 1.3,
"tools": [
@@ -176,7 +176,7 @@ Output response like :-
"id": "chat_67890",
"object": "chat.completion",
"created": 1677654322,
- "model": "deepseek-chat",
+ "model": "deepseek-v4-pro",
"choices": [
{
"index": 0,
@@ -190,3 +190,10 @@ Output response like :-
}
```
+---
+
+### Thinking-mode caveat
+
+When using V4 models with thinking mode enabled (or the legacy `DeepSeek-R1`), assistant responses include a `reasoning_content` field at the same level as `content`. **This field MUST be echoed back on the next tool turn**; otherwise the DeepSeek API returns HTTP 400.
+
+See the [DeepSeek reasoning model docs](https://api-docs.deepseek.com/guides/reasoning_model) for details. Helpers for reading `reasoning_content` off the response and passing it back into the next request will land in `v2.2.0` together with `setThinking()` / `setReasoningEffort()`.
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..91e6453
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,4 @@
+parameters:
+ level: 5
+ paths:
+ - src
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..7051518
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ ./tests/Unit
+
+
+ ./tests/Feature
+
+
+
+
+ ./src
+
+
+
diff --git a/src/Contracts/ClientContract.php b/src/Contracts/ClientContract.php
index 12172e3..c865a8a 100644
--- a/src/Contracts/ClientContract.php
+++ b/src/Contracts/ClientContract.php
@@ -5,10 +5,16 @@
interface ClientContract
{
public function run(): string;
+
public static function build(string $apiKey, ?string $baseUrl = null, ?int $timeout = null): self;
- public function query(string $content, ?string $role = "user"): self;
+
+ public function query(string $content, ?string $role = 'user'): self;
+
public function getModelsList(): self;
+
public function withModel(?string $model = null): self;
+
public function withStream(bool $stream = true): self;
+
public function buildQuery(string $content, ?string $role = null): array;
}
diff --git a/src/Contracts/Factories/ApiFactoryContract.php b/src/Contracts/Factories/ApiFactoryContract.php
index 7799e7e..646f48a 100644
--- a/src/Contracts/Factories/ApiFactoryContract.php
+++ b/src/Contracts/Factories/ApiFactoryContract.php
@@ -10,39 +10,32 @@ interface ApiFactoryContract
{
/**
* Create a new instance of the factory.
- *
- * @return ApiFactory
*/
public static function build(): ApiFactory;
/**
* Set the base URL for the API.
*
- * @param string|null $baseUrl The base URL to set (optional).
- * @return ApiFactory
+ * @param string|null $baseUrl The base URL to set (optional).
*/
public function setBaseUri(?string $baseUrl = null): ApiFactory;
/**
* Set the API key for authentication.
*
- * @param string $apiKey The API key to set.
- * @return ApiFactory
+ * @param string $apiKey The API key to set.
*/
public function setKey(string $apiKey): ApiFactory;
/**
* Set the timeout for the API request.
*
- * @param int|null $timeout The timeout value in seconds (optional).
- * @return ApiFactory
+ * @param int|null $timeout The timeout value in seconds (optional).
*/
public function setTimeout(?int $timeout = null): ApiFactory;
/**
* Build and return http Client instance.
- *
- * @return ClientInterface
*/
public function run(?string $clientType = null): ClientInterface;
}
diff --git a/src/Contracts/Models/ResultContract.php b/src/Contracts/Models/ResultContract.php
index 764c6f6..5c1b249 100644
--- a/src/Contracts/Models/ResultContract.php
+++ b/src/Contracts/Models/ResultContract.php
@@ -6,19 +6,16 @@ interface ResultContract
{
/**
* result status code
- * @return int
*/
public function getStatusCode(): int;
/**
* result content date as a string
- * @return string
*/
public function getContent(): string;
/**
* if response status code is ok (200)
- * @return bool
*/
public function isSuccess(): bool;
}
diff --git a/src/Contracts/Resources/ResourceContract.php b/src/Contracts/Resources/ResourceContract.php
index 9fc174c..2315767 100644
--- a/src/Contracts/Resources/ResourceContract.php
+++ b/src/Contracts/Resources/ResourceContract.php
@@ -9,22 +9,16 @@ interface ResourceContract
{
/**
* Get the endpoint suffix for the resource.
- *
- * @return string
*/
public function getEndpointSuffix(): string;
/**
* Get the model associated with the resource.
- *
- * @return string
*/
public function getDefaultModel(): string;
/**
* check if stream enabled or not.
- *
- * @return bool
*/
public function getDefaultStream(): bool;
}
diff --git a/src/DeepSeekClient.php b/src/DeepSeekClient.php
index 62e39f5..3d55b4e 100644
--- a/src/DeepSeekClient.php
+++ b/src/DeepSeekClient.php
@@ -4,16 +4,17 @@
use DeepSeek\Contracts\ClientContract;
use DeepSeek\Contracts\Models\ResultContract;
+use DeepSeek\Enums\Configs\TemperatureValues;
+use DeepSeek\Enums\Queries\QueryRoles;
use DeepSeek\Enums\Requests\ClientTypes;
use DeepSeek\Enums\Requests\EndpointSuffixes;
-use DeepSeek\Resources\Resource;
-use Psr\Http\Client\ClientInterface;
-use DeepSeek\Factories\ApiFactory;
-use DeepSeek\Enums\Queries\QueryRoles;
use DeepSeek\Enums\Requests\QueryFlags;
-use DeepSeek\Enums\Configs\TemperatureValues;
-use DeepSeek\Traits\Resources\{HasChat, HasCoder};
+use DeepSeek\Factories\ApiFactory;
+use DeepSeek\Resources\Resource;
use DeepSeek\Traits\Client\HasToolsFunctionCalling;
+use DeepSeek\Traits\Resources\HasChat;
+use DeepSeek\Traits\Resources\HasCoder;
+use Psr\Http\Client\ClientInterface;
class DeepSeekClient implements ClientContract
{
@@ -22,39 +23,32 @@ class DeepSeekClient implements ClientContract
/**
* PSR-18 HTTP client for making requests.
- *
- * @var ClientInterface
*/
protected ClientInterface $httpClient;
/**
* Array to store accumulated queries.
- *
- * @var array
*/
protected array $queries = [];
/**
* The model being used for API requests.
- *
- * @var string|null
*/
protected ?string $model;
/**
* Indicates whether to enable streaming for API responses.
- *
- * @var bool
*/
protected bool $stream;
protected float $temperature;
+
protected int $maxTokens;
+
protected string $responseFormatType;
/**
* response result contract
- * @var ResultContract
*/
protected ResultContract $result;
@@ -64,14 +58,13 @@ class DeepSeekClient implements ClientContract
/**
* Array of tools for using function calling.
- * @var array|null $tools
*/
protected ?array $tools;
/**
* Initialize the DeepSeekClient with a PSR-compliant HTTP client.
*
- * @param ClientInterface $httpClient The HTTP client used for making API requests.
+ * @param ClientInterface $httpClient The HTTP client used for making API requests.
*/
public function __construct(ClientInterface $httpClient)
{
@@ -90,26 +83,27 @@ public function run(): string
{
$requestData = [
QueryFlags::MESSAGES->value => $this->queries,
- QueryFlags::MODEL->value => $this->model,
- QueryFlags::STREAM->value => $this->stream,
- QueryFlags::TEMPERATURE->value => $this->temperature,
- QueryFlags::MAX_TOKENS->value => $this->maxTokens,
- QueryFlags::TOOLS->value => $this->tools,
- QueryFlags::RESPONSE_FORMAT->value => [
- 'type' => $this->responseFormatType
+ QueryFlags::MODEL->value => $this->model,
+ QueryFlags::STREAM->value => $this->stream,
+ QueryFlags::TEMPERATURE->value => $this->temperature,
+ QueryFlags::MAX_TOKENS->value => $this->maxTokens,
+ QueryFlags::TOOLS->value => $this->tools,
+ QueryFlags::RESPONSE_FORMAT->value => [
+ 'type' => $this->responseFormatType,
],
];
$this->setResult((new Resource($this->httpClient, $this->endpointSuffixes))->sendRequest($requestData, $this->requestMethod));
+
return $this->getResult()->getContent();
}
/**
* Create a new DeepSeekClient instance with the given API key.
*
- * @param string $apiKey The API key for authentication.
- * @param string|null $baseUrl The base URL for the API (optional).
- * @param int|null $timeout The timeout duration for requests in seconds (optional).
+ * @param string $apiKey The API key for authentication.
+ * @param string|null $baseUrl The base URL for the API (optional).
+ * @param int|null $timeout The timeout duration for requests in seconds (optional).
* @return self A new instance of the DeepSeekClient.
*/
public static function build(string $apiKey, ?string $baseUrl = null, ?int $timeout = null, ?string $clientType = null): self
@@ -128,24 +122,24 @@ public static function build(string $apiKey, ?string $baseUrl = null, ?int $time
/**
* Add a query to the accumulated queries list.
*
- * @param string $content
- * @param string|null $role
* @return self The current instance for method chaining.
*/
- public function query(string $content, ?string $role = "user"): self
+ public function query(string $content, ?string $role = 'user'): self
{
$this->queries[] = $this->buildQuery($content, $role);
+
return $this;
}
-
+
/**
* Reset a queries list to empty.
*
* @return self The current instance for method chaining.
*/
- public function resetQueries()
+ public function resetQueries(): self
{
$this->queries = [];
+
return $this;
}
@@ -158,48 +152,54 @@ public function getModelsList(): self
{
$this->endpointSuffixes = EndpointSuffixes::MODELS_LIST->value;
$this->requestMethod = 'GET';
+
return $this;
}
/**
* Set the model to be used for API requests.
*
- * @param string|null $model The model name (optional).
+ * @param string|null $model The model name (optional).
* @return self The current instance for method chaining.
*/
public function withModel(?string $model = null): self
{
$this->model = $model;
+
return $this;
}
/**
* Enable or disable streaming for API responses.
*
- * @param bool $stream Whether to enable streaming (default: true).
+ * @param bool $stream Whether to enable streaming (default: true).
* @return self The current instance for method chaining.
*/
public function withStream(bool $stream = true): self
{
$this->stream = $stream;
+
return $this;
}
public function setTemperature(float $temperature): self
{
$this->temperature = $temperature;
+
return $this;
}
public function setMaxTokens(int $maxTokens): self
{
$this->maxTokens = $maxTokens;
+
return $this;
}
public function setResponseFormat(string $type): self
{
$this->responseFormatType = $type;
+
return $this;
}
@@ -207,24 +207,24 @@ public function buildQuery(string $content, ?string $role = null): array
{
return [
'role' => $role ?: QueryRoles::USER->value,
- 'content' => $content
+ 'content' => $content,
];
}
/**
* set result model
- * @param \DeepSeek\Contracts\Models\ResultContract $result
+ *
* @return self The current instance for method chaining.
*/
- public function setResult(ResultContract $result)
+ public function setResult(ResultContract $result): self
{
$this->result = $result;
+
return $this;
}
/**
* response result model
- * @return \DeepSeek\Contracts\Models\ResultContract
*/
public function getResult(): ResultContract
{
diff --git a/src/Enums/Configs/DefaultConfigs.php b/src/Enums/Configs/DefaultConfigs.php
index 204c9d1..cd4a907 100644
--- a/src/Enums/Configs/DefaultConfigs.php
+++ b/src/Enums/Configs/DefaultConfigs.php
@@ -4,8 +4,8 @@
enum DefaultConfigs: string
{
- case BASE_URL = 'https://api.deepseek.com/v3';
- case MODEL = 'DeepSeek-R1';
+ case BASE_URL = 'https://api.deepseek.com';
+ case MODEL = 'deepseek-v4-flash';
case TIMEOUT = '30';
case STREAM = 'false';
}
diff --git a/src/Enums/Configs/TemperatureValues.php b/src/Enums/Configs/TemperatureValues.php
index c09d315..5ad9210 100644
--- a/src/Enums/Configs/TemperatureValues.php
+++ b/src/Enums/Configs/TemperatureValues.php
@@ -4,14 +4,45 @@
enum TemperatureValues: string
{
- case CODING = "0.0";
- case MATH = "0.1";
- case DATA_ANALYSIS = "1.0";
- case DATA_CLEANING = "1.1";
- case GENERAL_CONVERSATION = "1.3";
- case TRANSLATION = "1.4";
- case CREATIVE_WRITING = "1.5";
- case POETRY = "1.6";
- case MAX_TOKENS = "4096";
- case RESPONSE_FORMAT_TYPE = "text";
+ /** Recommended by DeepSeek docs: 0.0 for Coding / Math. */
+ case CODING = '0.0';
+
+ /**
+ * @deprecated Use CODING instead. DeepSeek docs recommend the same temperature (0.0)
+ * for both Coding and Math. Kept for backward compatibility.
+ * @see https://api-docs.deepseek.com/quick_start/parameter_settings
+ */
+ case MATH = '0.1';
+
+ /** Recommended by DeepSeek docs: 1.0 for Data Cleaning / Data Analysis. */
+ case DATA_ANALYSIS = '1.0';
+
+ /**
+ * @deprecated Use DATA_ANALYSIS instead. DeepSeek docs recommend the same temperature (1.0)
+ * for both Data Analysis and Data Cleaning. Kept for backward compatibility.
+ * @see https://api-docs.deepseek.com/quick_start/parameter_settings
+ */
+ case DATA_CLEANING = '1.1';
+
+ /** Recommended by DeepSeek docs: 1.3 for General Conversation / Translation. */
+ case GENERAL_CONVERSATION = '1.3';
+
+ /**
+ * @deprecated Use GENERAL_CONVERSATION instead. DeepSeek docs recommend the same temperature (1.3)
+ * for both General Conversation and Translation. Kept for backward compatibility.
+ * @see https://api-docs.deepseek.com/quick_start/parameter_settings
+ */
+ case TRANSLATION = '1.4';
+
+ /** Recommended by DeepSeek docs: 1.5 for Creative Writing / Poetry. */
+ case CREATIVE_WRITING = '1.5';
+
+ /**
+ * @deprecated Use CREATIVE_WRITING instead. DeepSeek docs recommend the same temperature (1.5)
+ * for both Creative Writing and Poetry. Kept for backward compatibility.
+ * @see https://api-docs.deepseek.com/quick_start/parameter_settings
+ */
+ case POETRY = '1.6';
+ case MAX_TOKENS = '4096';
+ case RESPONSE_FORMAT_TYPE = 'text';
}
diff --git a/src/Enums/Models.php b/src/Enums/Models.php
index ff699ed..4cbcec3 100644
--- a/src/Enums/Models.php
+++ b/src/Enums/Models.php
@@ -4,8 +4,56 @@
enum Models: string
{
+ /**
+ * @deprecated since 2.1.0, will be removed in 3.0.0.
+ * The 'deepseek-chat' alias retires from the DeepSeek API on 2026-07-24
+ * (currently routes to deepseek-v4-flash non-thinking mode).
+ * Use Models::V4_FLASH or Models::V4_PRO instead.
+ */
case CHAT = 'deepseek-chat';
+
+ /**
+ * @deprecated since 2.1.0, will be removed in 3.0.0.
+ * The 'deepseek-reasoner' alias retires from the DeepSeek API on 2026-07-24
+ * (currently routes to deepseek-v4-flash thinking mode).
+ * Use Models::V4_FLASH with thinking mode instead.
+ */
+ case REASONER = 'deepseek-reasoner';
+
+ /**
+ * @deprecated since 2.1.0, will be removed in 3.0.0.
+ * The 'deepseek-coder' model no longer exists in the DeepSeek API.
+ * Use Models::V4_PRO or Models::V4_FLASH instead.
+ */
case CODER = 'deepseek-coder';
+
+ /**
+ * @deprecated since 2.1.0, will be removed in 3.0.0.
+ * The 'DeepSeek-R1' alias retires from the DeepSeek API on 2026-07-24
+ * (currently routes to deepseek-v4-flash thinking mode).
+ * Use Models::V4_FLASH with setThinking() (available v2.2.0+) instead.
+ */
case R1 = 'DeepSeek-R1';
+
+ /**
+ * @deprecated since 2.1.0, will be removed in 3.0.0.
+ * 'DeepSeek-R1-Zero' was never a valid DeepSeek API model id.
+ */
case R1Zero = 'DeepSeek-R1-Zero';
+
+ /**
+ * DeepSeek-V4-Pro: flagship model. 1M context, max 384K output tokens.
+ * Supports both thinking and non-thinking modes.
+ *
+ * @see https://api-docs.deepseek.com/quick_start/pricing
+ */
+ case V4_PRO = 'deepseek-v4-pro';
+
+ /**
+ * DeepSeek-V4-Flash: fast, efficient, economical. 1M context, max 384K output tokens.
+ * Supports both thinking and non-thinking modes.
+ *
+ * @see https://api-docs.deepseek.com/quick_start/pricing
+ */
+ case V4_FLASH = 'deepseek-v4-flash';
}
diff --git a/src/Factories/ApiFactory.php b/src/Factories/ApiFactory.php
index 0a38466..aa71bb4 100644
--- a/src/Factories/ApiFactory.php
+++ b/src/Factories/ApiFactory.php
@@ -7,62 +7,68 @@
use DeepSeek\Enums\Requests\ClientTypes;
use DeepSeek\Enums\Requests\HeaderFlags;
use GuzzleHttp\Client;
+use InvalidArgumentException;
use Psr\Http\Client\ClientInterface;
+use RuntimeException;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;
-use RuntimeException;
-use InvalidArgumentException;
final class ApiFactory implements ApiFactoryContract
{
protected string $apiKey;
+
protected string $baseUrl;
+
protected int $timeout;
+
protected array $clientConfig;
public static function build(): self
{
- return new self();
+ return new self;
}
public function setBaseUri(?string $baseUrl = null): self
{
$this->baseUrl = $baseUrl ? trim($baseUrl) : DefaultConfigs::BASE_URL->value;
+
return $this;
}
public function setKey(string $apiKey): self
{
$this->apiKey = trim($apiKey);
+
return $this;
}
public function setTimeout(?int $timeout = null): self
{
- $this->timeout = $timeout ?: (int)DefaultConfigs::TIMEOUT->value;
+ $this->timeout = $timeout ?: (int) DefaultConfigs::TIMEOUT->value;
+
return $this;
}
public function initialize(): self
{
- if (!isset($this->baseUrl)) {
+ if (! isset($this->baseUrl)) {
$this->setBaseUri();
}
- if (!isset($this->apiKey)) {
+ if (! isset($this->apiKey)) {
throw new RuntimeException('API key must be set using setKey() before initialization.');
}
- if (!isset($this->timeout)) {
+ if (! isset($this->timeout)) {
$this->setTimeout();
}
$this->clientConfig = [
HeaderFlags::BASE_URL->value => $this->baseUrl,
- HeaderFlags::TIMEOUT->value => $this->timeout,
- HeaderFlags::HEADERS->value => [
- HeaderFlags::AUTHORIZATION->value => 'Bearer ' . $this->apiKey,
- HeaderFlags::CONTENT_TYPE->value => 'application/json',
+ HeaderFlags::TIMEOUT->value => $this->timeout,
+ HeaderFlags::HEADERS->value => [
+ HeaderFlags::AUTHORIZATION->value => 'Bearer '.$this->apiKey,
+ HeaderFlags::CONTENT_TYPE->value => 'application/json',
],
];
@@ -73,7 +79,7 @@ public function run(?string $clientType = null): ClientInterface
{
$clientType = $clientType ?? ClientTypes::GUZZLE->value;
- if (!isset($this->clientConfig)) {
+ if (! isset($this->clientConfig)) {
$this->initialize();
}
diff --git a/src/Models/BadResult.php b/src/Models/BadResult.php
index 5bc9608..3b67e8e 100644
--- a/src/Models/BadResult.php
+++ b/src/Models/BadResult.php
@@ -4,8 +4,4 @@
namespace DeepSeek\Models;
-class BadResult extends ResultAbstract
-{
-
-}
-
+class BadResult extends ResultAbstract {}
diff --git a/src/Models/FailureResult.php b/src/Models/FailureResult.php
index b8fb272..2b7be45 100644
--- a/src/Models/FailureResult.php
+++ b/src/Models/FailureResult.php
@@ -4,8 +4,4 @@
namespace DeepSeek\Models;
-class FailureResult extends ResultAbstract
-{
-
-}
-
+class FailureResult extends ResultAbstract {}
diff --git a/src/Models/ResultAbstract.php b/src/Models/ResultAbstract.php
index a5b121a..2afc6e4 100644
--- a/src/Models/ResultAbstract.php
+++ b/src/Models/ResultAbstract.php
@@ -11,47 +11,61 @@
abstract class ResultAbstract implements ResultContract
{
protected ?int $statusCode;
+
protected ?string $content;
+
/**
* handel response coming from request
- * @var ResponseInterface|null
*/
protected ?ResponseInterface $response;
+
public function __construct(?int $statusCode = null, ?string $content = null)
{
$this->statusCode = $statusCode;
$this->content = $content;
}
- protected function setStatusCode(int $statusCode)
+
+ protected function setStatusCode(int $statusCode): void
{
$this->statusCode = $statusCode;
}
+
public function getStatusCode(): int
{
return $this->statusCode;
}
+
protected function setContent(string $content): void
{
- $this->content = $content;
+ // Strip DeepSeek Keep-Alive padding per
+ // https://api-docs.deepseek.com/quick_start/rate_limit#request-keep-alive-mechanism
+ // - Non-streaming responses: leading empty lines before the JSON body
+ // - Streaming responses: ": keep-alive" SSE comment lines anywhere in the stream
+ $content = preg_replace('/^: keep-alive\R/m', '', $content) ?? $content;
+ $this->content = ltrim($content, "\r\n");
}
+
public function getContent(): string
{
return $this->content;
}
+
public function setResponse(ResponseInterface $response): static
{
$this->response = $response;
$this->setStatusCode($this->getResponse()->getStatusCode());
$this->setContent($this->getResponse()->getBody()->getContents());
+
return $this;
}
+
public function getResponse(): ResponseInterface
{
return $this->response;
}
+
public function isSuccess(): bool
{
- return ($this->getStatusCode() === HTTPState::OK->value);
+ return $this->getStatusCode() === HTTPState::OK->value;
}
}
-
diff --git a/src/Models/SuccessResult.php b/src/Models/SuccessResult.php
index 2cd0fd3..e56bb87 100644
--- a/src/Models/SuccessResult.php
+++ b/src/Models/SuccessResult.php
@@ -4,8 +4,4 @@
namespace DeepSeek\Models;
-class SuccessResult extends ResultAbstract
-{
-
-}
-
+class SuccessResult extends ResultAbstract {}
diff --git a/src/Resources/Chat.php b/src/Resources/Chat.php
index 1863a52..a0edd19 100644
--- a/src/Resources/Chat.php
+++ b/src/Resources/Chat.php
@@ -4,7 +4,4 @@
namespace DeepSeek\Resources;
-class Chat extends Resource
-{
-
-}
+class Chat extends Resource {}
diff --git a/src/Resources/Coder.php b/src/Resources/Coder.php
index 412beed..99cc9ec 100644
--- a/src/Resources/Coder.php
+++ b/src/Resources/Coder.php
@@ -17,6 +17,6 @@ class Coder extends Resource
*/
public function getDefaultModel(): string
{
- return Models::CODER->value;
+ return Models::V4_FLASH->value;
}
}
diff --git a/src/Resources/Resource.php b/src/Resources/Resource.php
index 65ed2fe..ed4fc41 100644
--- a/src/Resources/Resource.php
+++ b/src/Resources/Resource.php
@@ -7,8 +7,8 @@
use DeepSeek\Contracts\Models\ResultContract;
use DeepSeek\Contracts\Resources\ResourceContract;
use DeepSeek\Enums\Configs\DefaultConfigs;
-use DeepSeek\Enums\Models;
use DeepSeek\Enums\Data\DataTypes;
+use DeepSeek\Enums\Models;
use DeepSeek\Enums\Requests\EndpointSuffixes;
use DeepSeek\Enums\Requests\QueryFlags;
use DeepSeek\Models\BadResult;
@@ -17,18 +17,21 @@
use DeepSeek\Traits\Queries\HasQueryParams;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\GuzzleException;
+use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
-use Nyholm\Psr7\Factory\Psr17Factory;
class Resource implements ResourceContract
{
use HasQueryParams;
protected ClientInterface $client;
+
protected ?string $endpointSuffixes;
+
protected RequestFactoryInterface $requestFactory;
+
protected StreamFactoryInterface $streamFactory;
public function __construct(
@@ -39,8 +42,8 @@ public function __construct(
) {
$this->client = $client;
$this->endpointSuffixes = $endpointSuffixes ?: EndpointSuffixes::CHAT->value;
- $this->requestFactory = $requestFactory ?: new Psr17Factory();
- $this->streamFactory = $streamFactory ?: new Psr17Factory();
+ $this->requestFactory = $requestFactory ?: new Psr17Factory;
+ $this->streamFactory = $streamFactory ?: new Psr17Factory;
}
public function sendRequest(array $requestData, ?string $requestMethod = 'POST'): ResultContract
@@ -62,9 +65,9 @@ public function sendRequest(array $requestData, ?string $requestMethod = 'POST')
$response = $this->client->sendRequest($request);
- return (new SuccessResult())->setResponse($response);
+ return (new SuccessResult)->setResponse($response);
} catch (BadResponseException $badResponse) {
- return (new BadResult())->setResponse($badResponse->getResponse());
+ return (new BadResult)->setResponse($badResponse->getResponse());
} catch (GuzzleException $error) {
return new FailureResult($error->getCode(), $error->getMessage());
} catch (\Exception $error) {
@@ -78,7 +81,7 @@ public function sendRequest(array $requestData, ?string $requestMethod = 'POST')
* This method merges the given query data with custom headers that are
* prepared for the request.
*
- * @param array $requestData The data to send in the request.
+ * @param array $requestData The data to send in the request.
* @return array The merged request data with default headers.
*/
protected function resolveHeaders(array $requestData): array
@@ -92,7 +95,7 @@ protected function resolveHeaders(array $requestData): array
* This method loops through the query parameters and applies the appropriate
* type conversion before returning the final headers.
*
- * @param array $query The data to send in the request.
+ * @param array $query The data to send in the request.
* @return array The custom headers for the request.
*/
public function prepareCustomHeaderParams(array $query): array
@@ -129,7 +132,7 @@ public function getEndpointSuffix(): string
*/
public function getDefaultModel(): string
{
- return Models::CHAT->value;
+ return Models::V4_FLASH->value;
}
/**
@@ -142,7 +145,7 @@ public function getDefaultModel(): string
*/
public function getDefaultStream(): bool
{
- return DefaultConfigs::STREAM->value === 'true';
+ return filter_var(DefaultConfigs::STREAM->value, FILTER_VALIDATE_BOOLEAN);
}
/**
diff --git a/src/Traits/Client/HasToolsFunctionCalling.php b/src/Traits/Client/HasToolsFunctionCalling.php
index 97d53fc..9cae45b 100644
--- a/src/Traits/Client/HasToolsFunctionCalling.php
+++ b/src/Traits/Client/HasToolsFunctionCalling.php
@@ -7,26 +7,26 @@
trait HasToolsFunctionCalling
{
/**
- * @param array $tools A list of tools the model may call.
+ * @param array $tools A list of tools the model may call.
* @return self The current instance for method chaining.
*/
public function setTools(array $tools): self
{
$this->tools = $tools;
+
return $this;
}
/**
* Add a query tool calls to the accumulated queries list.
*
- * @param array $toolCalls The tool calls generated by the model, such as function calls.
- * @param string $content
- * @param string|null $role
+ * @param array $toolCalls The tool calls generated by the model, such as function calls.
* @return self The current instance for method chaining.
*/
public function queryToolCall(array $toolCalls, string $content, ?string $role = null): self
{
$this->queries[] = $this->buildToolCallQuery($toolCalls, $content, $role);
+
return $this;
}
@@ -37,20 +37,19 @@ public function buildToolCallQuery(array $toolCalls, string $content, ?string $r
'tool_calls' => $toolCalls,
'content' => $content,
];
+
return $query;
}
/**
* Add a query tool to the accumulated queries list.
*
- * @param string $toolCallId
- * @param string $content
- * @param string|null $role
* @return self The current instance for method chaining.
*/
- public function queryTool(string $toolCallId, string $content , ?string $role = null): self
+ public function queryTool(string $toolCallId, string $content, ?string $role = null): self
{
$this->queries[] = $this->buildToolQuery($toolCallId, $content, $role);
+
return $this;
}
@@ -61,6 +60,7 @@ public function buildToolQuery(string $toolCallId, string $content, ?string $rol
'tool_call_id' => $toolCallId,
'content' => $content,
];
+
return $query;
}
}
diff --git a/src/Traits/Queries/HasQueryParams.php b/src/Traits/Queries/HasQueryParams.php
index bb2a1a8..aafa3cd 100644
--- a/src/Traits/Queries/HasQueryParams.php
+++ b/src/Traits/Queries/HasQueryParams.php
@@ -10,16 +10,13 @@ trait HasQueryParams
/**
* Helper method to get the query parameter or default value with type conversion.
*
- * @param array $query
- * @param string $key
- * @param mixed $default
- * @param string $type
- * @return mixed
+ * @param mixed $default
*/
private function getQueryParam(array $query, string $key, $default, string $type): mixed
{
if (isset($query[$key])) {
$value = $query[$key];
+
return $this->convertValue($value, $type);
}
@@ -29,29 +26,24 @@ private function getQueryParam(array $query, string $key, $default, string $type
/**
* Convert the value to the specified type.
*
- * @param mixed $value
- * @param string $type
- * @return mixed
+ * @param mixed $value
*/
private function convertValue($value, string $type): mixed
{
return match ($type) {
- DataTypes::STRING->value => (string)$value,
- DataTypes::INTEGER->value => (int)$value,
- DataTypes::FLOAT->value => (float)$value,
- DataTypes::ARRAY->value => (array)$value,
- DataTypes::OBJECT->value => (object)$value,
- DataTypes::BOOL->value => (bool)$value,
- DataTypes::JSON->value => json_decode((string)$value, true),
+ DataTypes::STRING->value => (string) $value,
+ DataTypes::INTEGER->value => (int) $value,
+ DataTypes::FLOAT->value => (float) $value,
+ DataTypes::ARRAY->value => (array) $value,
+ DataTypes::OBJECT->value => (object) $value,
+ DataTypes::BOOL->value => (bool) $value,
+ DataTypes::JSON->value => json_decode((string) $value, true),
default => $value,
};
}
/**
* Get default value for specific query keys.
- *
- * @param string $key
- * @return mixed
*/
private function getDefaultForKey(string $key): mixed
{
diff --git a/src/Traits/Resources/HasChat.php b/src/Traits/Resources/HasChat.php
index 02cabdd..b03063e 100644
--- a/src/Traits/Resources/HasChat.php
+++ b/src/Traits/Resources/HasChat.php
@@ -2,6 +2,7 @@
namespace DeepSeek\Traits\Resources;
+use DeepSeek\Enums\Requests\QueryFlags;
use DeepSeek\Resources\Chat;
trait HasChat
@@ -9,17 +10,26 @@ trait HasChat
/**
* Send the accumulated queries to the Chat resource.
*
- * @return string
+ * Since 2.1.0, this shortcut honors the same configuration as run():
+ * temperature, max_tokens, tools, and response_format set via the
+ * corresponding setters are now included in the request body.
*/
public function chat(): string
{
$requestData = [
- 'messages' => $this->queries,
- 'model' => $this->model,
- 'stream' => $this->stream,
+ QueryFlags::MESSAGES->value => $this->queries,
+ QueryFlags::MODEL->value => $this->model,
+ QueryFlags::STREAM->value => $this->stream,
+ QueryFlags::TEMPERATURE->value => $this->temperature,
+ QueryFlags::MAX_TOKENS->value => $this->maxTokens,
+ QueryFlags::TOOLS->value => $this->tools,
+ QueryFlags::RESPONSE_FORMAT->value => [
+ 'type' => $this->responseFormatType,
+ ],
];
$this->queries = [];
$this->setResult((new Chat($this->httpClient))->sendRequest($requestData));
+
return $this->getResult()->getContent();
}
}
diff --git a/src/Traits/Resources/HasCoder.php b/src/Traits/Resources/HasCoder.php
index 9a684eb..4ef5d52 100644
--- a/src/Traits/Resources/HasCoder.php
+++ b/src/Traits/Resources/HasCoder.php
@@ -2,6 +2,7 @@
namespace DeepSeek\Traits\Resources;
+use DeepSeek\Enums\Requests\QueryFlags;
use DeepSeek\Resources\Coder;
trait HasCoder
@@ -9,17 +10,26 @@ trait HasCoder
/**
* Send the accumulated queries to the code resource.
*
- * @return string
+ * Since 2.1.0, this shortcut honors the same configuration as run():
+ * temperature, max_tokens, tools, and response_format set via the
+ * corresponding setters are now included in the request body.
*/
public function code(): string
{
$requestData = [
- 'messages' => $this->queries,
- 'model' => $this->model,
- 'stream' => $this->stream,
+ QueryFlags::MESSAGES->value => $this->queries,
+ QueryFlags::MODEL->value => $this->model,
+ QueryFlags::STREAM->value => $this->stream,
+ QueryFlags::TEMPERATURE->value => $this->temperature,
+ QueryFlags::MAX_TOKENS->value => $this->maxTokens,
+ QueryFlags::TOOLS->value => $this->tools,
+ QueryFlags::RESPONSE_FORMAT->value => [
+ 'type' => $this->responseFormatType,
+ ],
];
$this->queries = [];
$this->setResult((new Coder($this->httpClient))->sendRequest($requestData));
+
return $this->getResult()->getContent();
}
}
diff --git a/tests/Feature/ClientDependency/FakeResponse.php b/tests/Feature/ClientDependency/FakeResponse.php
index 3b1b725..f2d3d53 100644
--- a/tests/Feature/ClientDependency/FakeResponse.php
+++ b/tests/Feature/ClientDependency/FakeResponse.php
@@ -2,11 +2,11 @@
namespace Tests\Feature\ClientDependency;
-class FakeResponse
+class FakeResponse
{
public function toolFunctionCalling()
{
- return <<createStream('{"id":"abc","object":"chat.completion","choices":[{"message":{"content":"Hello!","role":"assistant"}}]}');
+
+ /** @var ClientInterface&LegacyMockInterface&MockInterface $httpClient */
+ $httpClient = Mockery::mock(ClientInterface::class);
+ $httpClient->shouldReceive('sendRequest')->once()->andReturn(
+ $factory->createResponse(200)->withBody($body)
+ );
+
+ $client = (new DeepSeekClient($httpClient))
->query('Hello DeepSeek, how are you today?')
->setTemperature(1.5);
- // Act
$response = $client->run();
$result = $client->getResult();
- // Assert
- expect($response)->not->toBeEmpty($response)
+ expect($response)->not->toBeEmpty()
->and($result->getStatusCode())->toEqual(HTTPState::OK->value);
});
test('Run query with valid API Key & insufficient balance should return 402', function () {
- // Arrange
- $apiKey = "insufficient-balance-api-key";
- $client = DeepSeekClient::build($apiKey)
+ $factory = new Psr17Factory;
+ $body = $factory->createStream('{"error":{"message":"Insufficient balance","type":"insufficient_quota"}}');
+
+ /** @var ClientInterface&LegacyMockInterface&MockInterface $httpClient */
+ $httpClient = Mockery::mock(ClientInterface::class);
+ $httpClient->shouldReceive('sendRequest')->once()->andReturn(
+ $factory->createResponse(402)->withBody($body)
+ );
+
+ $client = (new DeepSeekClient($httpClient))
->query('Hello DeepSeek, how are you today?')
->setTemperature(1.5);
- // Act
$response = $client->run();
$result = $client->getResult();
- // Assert
- expect($response)->not->toBeEmpty($response)
+ expect($response)->not->toBeEmpty()
->and($result->getStatusCode())->toEqual(HTTPState::PAYMENT_REQUIRED->value);
});
test('Run query with invalid API key should return 401', function () {
- // Arrange
- $apiKey = "insufficient-balance-api-key";
- $client = DeepSeekClient::build($apiKey)
+ $factory = new Psr17Factory;
+ $body = $factory->createStream('{"error":{"message":"Authentication Fails","type":"authentication_error"}}');
+
+ /** @var ClientInterface&LegacyMockInterface&MockInterface $httpClient */
+ $httpClient = Mockery::mock(ClientInterface::class);
+ $httpClient->shouldReceive('sendRequest')->once()->andReturn(
+ $factory->createResponse(401)->withBody($body)
+ );
+
+ $client = (new DeepSeekClient($httpClient))
->query('Hello DeepSeek, how are you today?')
->setTemperature(1.5);
- // Act
$response = $client->run();
$result = $client->getResult();
- // Assert
- expect($response)->not->toBeEmpty($response)
+ expect($response)->not->toBeEmpty()
->and($result->getStatusCode())->toEqual(HTTPState::UNAUTHORIZED->value);
});
diff --git a/tests/Feature/FunctionCallingTest.php b/tests/Feature/FunctionCallingTest.php
index 041e153..71a9b58 100644
--- a/tests/Feature/FunctionCallingTest.php
+++ b/tests/Feature/FunctionCallingTest.php
@@ -1,157 +1,194 @@
["temperature"=> 22, "condition" => "Sunny"],
- "gharbia" => ["temperature"=> 23, "condition" => "Sunny"],
- "sharkia" => ["temperature"=> 24, "condition" => "Sunny"],
- "beheira" => ["temperature"=> 21, "condition" => "Sunny"],
- default => "not found city name."
+ $city = match ($city) {
+ 'cairo' => ['temperature' => 22, 'condition' => 'Sunny'],
+ 'gharbia' => ['temperature' => 23, 'condition' => 'Sunny'],
+ 'sharkia' => ['temperature' => 24, 'condition' => 'Sunny'],
+ 'beheira' => ['temperature' => 21, 'condition' => 'Sunny'],
+ default => 'not found city name.'
};
+
return json_encode($city);
}
test('Test function calling with fake responses.', function () {
// Arrange
- $fake = new FakeResponse();
+ $fake = new FakeResponse;
/** @var DeepSeekClient&LegacyMockInterface&MockInterface */
- $mockClient = Mockery::mock(DeepSeekClient::class);
-
+ $mockClient = Mockery::mock(DeepSeekClient::class);
+
$mockClient->shouldReceive('build')->andReturn($mockClient);
$mockClient->shouldReceive('setTools')->andReturn($mockClient);
$mockClient->shouldReceive('query')->andReturn($mockClient);
$mockClient->shouldReceive('run')->once()->andReturn($fake->toolFunctionCalling());
-
+
// Act
$response = $mockClient::build('your-api-key')
->query('What is the weather like in Cairo?')
->setTools([
[
- "type" => "function",
- "function" => [
- "name" => "get_weather",
- "description" => "Get the current weather in a given city",
- "parameters" => [
- "type" => "object",
- "properties" => [
- "city" => [
- "type" => "string",
- "description" => "The city name",
+ 'type' => 'function',
+ 'function' => [
+ 'name' => 'get_weather',
+ 'description' => 'Get the current weather in a given city',
+ 'parameters' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'city' => [
+ 'type' => 'string',
+ 'description' => 'The city name',
],
],
- "required" => ["city"],
+ 'required' => ['city'],
],
],
],
]
- )->run();
+ )->run();
// Assert
expect($fake->toolFunctionCalling())->toEqual($response);
-
- //------------------------------------------
-
- // Arrange
+
+ // ------------------------------------------
+
+ // Arrange
$response = json_decode($response, true);
$message = $response['choices'][0]['message'];
-
+
$firstFunction = $message['tool_calls'][0];
- if ($firstFunction['function']['name'] == "get_weather")
- {
+ if ($firstFunction['function']['name'] == 'get_weather') {
$weather_data = get_weather($firstFunction['function']['arguments']['city']);
}
$mockClient->shouldReceive('queryCallTool')->andReturn($mockClient);
$mockClient->shouldReceive('queryTool')->andReturn($mockClient);
$mockClient->shouldReceive('run')->andReturn($fake->resultToolFunctionCalling());
-
+
// Act
$response2 = $mockClient->queryCallTool(
- $message['tool_calls'],
- $message['content'],
- $message['role']
- )->queryTool(
- $firstFunction['id'],
- $weather_data,
- 'tool'
+ $message['tool_calls'],
+ $message['content'],
+ $message['role']
+ )->queryTool(
+ $firstFunction['id'],
+ $weather_data,
+ 'tool'
)->run();
// Assert
expect($fake->resultToolFunctionCalling())->toEqual($response2);
});
-test('Test function calling use base data with real responses.', function () {
- // Arrange
- $client = DeepSeekClient::build('your-api-key')
+afterEach(function () {
+ Mockery::close();
+});
+
+test('Test function calling use base data with mocked responses.', function () {
+ $factory = new Psr17Factory;
+
+ // First HTTP response: model asks to call get_weather
+ $toolCallBody = (string) json_encode([
+ 'id' => 'test-fc-1',
+ 'choices' => [[
+ 'finish_reason' => 'tool_calls',
+ 'index' => 0,
+ 'message' => [
+ 'content' => '',
+ 'role' => 'assistant',
+ 'tool_calls' => [[
+ 'id' => 'call-abc123',
+ 'type' => 'function',
+ 'function' => [
+ 'name' => 'get_weather',
+ 'arguments' => json_encode(['city' => 'Cairo']),
+ ],
+ ]],
+ ],
+ ]],
+ 'usage' => ['completion_tokens' => 12, 'prompt_tokens' => 20, 'total_tokens' => 32],
+ ]);
+
+ // Second HTTP response: model returns final answer after receiving tool result
+ $finalBody = (string) json_encode([
+ 'id' => 'test-fc-2',
+ 'choices' => [[
+ 'finish_reason' => 'stop',
+ 'index' => 0,
+ 'message' => [
+ 'content' => 'The weather in Cairo is sunny with a temperature of 22 degrees.',
+ 'role' => 'assistant',
+ ],
+ ]],
+ 'usage' => ['completion_tokens' => 20, 'prompt_tokens' => 40, 'total_tokens' => 60],
+ ]);
+
+ /** @var ClientInterface&LegacyMockInterface&MockInterface $httpClient */
+ $httpClient = Mockery::mock(ClientInterface::class);
+ $httpClient->shouldReceive('sendRequest')
+ ->andReturn(
+ $factory->createResponse(200)->withBody($factory->createStream($toolCallBody)),
+ $factory->createResponse(200)->withBody($factory->createStream($finalBody))
+ );
+
+ $client = new DeepSeekClient($httpClient);
+
+ // Act — first call: model responds with a tool call
+ $response = $client
->query('What is the weather like in Cairo?')
- ->setTools([
- [
- "type" => "function",
- "function" => [
- "name" => "get_weather",
- "description" => "Get the current weather in a given city",
- "parameters" => [
- "type" => "object",
- "properties" => [
- "city" => [
- "type" => "string",
- "description" => "The city name",
- ],
- ],
- "required" => ["city"],
+ ->setTools([[
+ 'type' => 'function',
+ 'function' => [
+ 'name' => 'get_weather',
+ 'description' => 'Get the current weather in a given city',
+ 'parameters' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'city' => ['type' => 'string', 'description' => 'The city name'],
],
+ 'required' => ['city'],
],
],
- ]
- );
-
- // Act
- $response = $client->run();
+ ]])
+ ->run();
$result = $client->getResult();
- // Assert
- expect($response)->not()->toBeEmpty($response)
+ // Assert first response
+ expect($response)->not()->toBeEmpty()
->and($result->getStatusCode())->toEqual(HTTPState::OK->value);
- //-----------------------------------------------------------------
-
- // Arrange
- $response = json_decode($response, true);
-
- $message = $response['choices'][0]['message'];
+ // Execute the tool locally
+ $decoded = json_decode($response, true);
+ $message = $decoded['choices'][0]['message'];
$firstFunction = $message['tool_calls'][0];
- if ($firstFunction['function']['name'] == "get_weather")
- {
+ $weather_data = '';
+ if ($firstFunction['function']['name'] === 'get_weather') {
$args = json_decode($firstFunction['function']['arguments'], true);
$weather_data = get_weather($args['city']);
}
-
- $client2 = $client->queryToolCall(
- $message['tool_calls'],
- $message['content'],
- $message['role']
- )->queryTool(
- $firstFunction['id'],
- $weather_data,
- 'tool'
- );
-
- // Act
- $response2 = $client2->run();
- $result2 = $client2->getResult();
- // Assert
- expect($response2)->not()->toBeEmpty($response2)
+ // Act — second call: send tool result back to model
+ $response2 = $client
+ ->queryToolCall($message['tool_calls'], $message['content'], $message['role'])
+ ->queryTool($firstFunction['id'], $weather_data, 'tool')
+ ->run();
+ $result2 = $client->getResult();
+
+ // Assert final response
+ expect($response2)->not()->toBeEmpty()
->and($result2->getStatusCode())->toEqual(HTTPState::OK->value);
});
diff --git a/tests/Feature/V210ChangesTest.php b/tests/Feature/V210ChangesTest.php
new file mode 100644
index 0000000..2416ebc
--- /dev/null
+++ b/tests/Feature/V210ChangesTest.php
@@ -0,0 +1,146 @@
+value)->toBe('deepseek-v4-pro')
+ ->and(Models::V4_FLASH->value)->toBe('deepseek-v4-flash');
+});
+
+test('default base URL no longer includes the /v3 suffix', function () {
+ expect(DefaultConfigs::BASE_URL->value)->toBe('https://api.deepseek.com');
+});
+
+test('legacy Models cases remain functional for backward compatibility', function () {
+ expect(Models::CHAT->value)->toBe('deepseek-chat')
+ ->and(Models::CODER->value)->toBe('deepseek-coder')
+ ->and(Models::R1->value)->toBe('DeepSeek-R1')
+ ->and(Models::R1Zero->value)->toBe('DeepSeek-R1-Zero');
+});
+
+test('SuccessResult strips leading empty lines (non-streaming keep-alive)', function () {
+ $factory = new Psr17Factory;
+ $body = $factory->createStream("\n\n\n".'{"id":"abc","object":"chat.completion"}');
+ $response = $factory->createResponse(200)->withBody($body);
+
+ $result = (new SuccessResult)->setResponse($response);
+
+ expect($result->getContent())->toBe('{"id":"abc","object":"chat.completion"}');
+});
+
+test('SuccessResult strips ": keep-alive" SSE comments (streaming)', function () {
+ $factory = new Psr17Factory;
+ $body = $factory->createStream(
+ "data: {\"chunk\":1}\n".
+ ": keep-alive\n".
+ "data: {\"chunk\":2}\n".
+ ": keep-alive\n".
+ "data: [DONE]\n"
+ );
+ $response = $factory->createResponse(200)->withBody($body);
+
+ $result = (new SuccessResult)->setResponse($response);
+
+ expect($result->getContent())->not->toContain(': keep-alive')
+ ->and($result->getContent())->toContain('data: {"chunk":1}')
+ ->and($result->getContent())->toContain('data: {"chunk":2}')
+ ->and($result->getContent())->toContain('data: [DONE]');
+});
+
+test('SuccessResult does not corrupt already-clean responses', function () {
+ $factory = new Psr17Factory;
+ $clean = '{"id":"xyz","object":"chat.completion","choices":[]}';
+ $body = $factory->createStream($clean);
+ $response = $factory->createResponse(200)->withBody($body);
+
+ $result = (new SuccessResult)->setResponse($response);
+
+ expect($result->getContent())->toBe($clean);
+});
+
+test('chat() shortcut includes temperature, max_tokens, tools, response_format in the request body', function () {
+ $factory = new Psr17Factory;
+ $capturedBody = null;
+
+ /** @var ClientInterface&LegacyMockInterface&MockInterface $httpClient */
+ $httpClient = Mockery::mock(ClientInterface::class);
+ $httpClient->shouldReceive('sendRequest')
+ ->once()
+ ->andReturnUsing(function (RequestInterface $request) use ($factory, &$capturedBody): ResponseInterface {
+ $capturedBody = (string) $request->getBody();
+
+ return $factory->createResponse(200)->withBody($factory->createStream('{"id":"ok"}'));
+ });
+
+ $tools = [[
+ 'type' => 'function',
+ 'function' => ['name' => 'noop', 'description' => 'noop'],
+ ]];
+
+ (new DeepSeekClient($httpClient))
+ ->withModel(Models::V4_FLASH->value)
+ ->setTemperature(0.7)
+ ->setMaxTokens(2048)
+ ->setResponseFormat('text')
+ ->setTools($tools)
+ ->query('Hello')
+ ->chat();
+
+ $decoded = json_decode((string) $capturedBody, true);
+
+ expect($decoded)
+ ->toBeArray()
+ ->toHaveKeys(['messages', 'model', 'stream', 'temperature', 'max_tokens', 'tools', 'response_format'])
+ ->and($decoded['model'])->toBe('deepseek-v4-flash')
+ ->and($decoded['temperature'])->toBe(0.7)
+ ->and($decoded['max_tokens'])->toBe(2048)
+ ->and($decoded['response_format'])->toBe(['type' => 'text'])
+ ->and($decoded['tools'])->toBe($tools);
+});
+
+test('code() shortcut includes temperature, max_tokens, tools, response_format in the request body', function () {
+ $factory = new Psr17Factory;
+ $capturedBody = null;
+
+ /** @var ClientInterface&LegacyMockInterface&MockInterface $httpClient */
+ $httpClient = Mockery::mock(ClientInterface::class);
+ $httpClient->shouldReceive('sendRequest')
+ ->once()
+ ->andReturnUsing(function (RequestInterface $request) use ($factory, &$capturedBody): ResponseInterface {
+ $capturedBody = (string) $request->getBody();
+
+ return $factory->createResponse(200)->withBody($factory->createStream('{"id":"ok"}'));
+ });
+
+ (new DeepSeekClient($httpClient))
+ ->withModel(Models::V4_PRO->value)
+ ->setTemperature(0.5)
+ ->setMaxTokens(1024)
+ ->query('def fib(n):')
+ ->code();
+
+ $decoded = json_decode((string) $capturedBody, true);
+
+ expect($decoded)
+ ->toBeArray()
+ ->toHaveKeys(['messages', 'model', 'stream', 'temperature', 'max_tokens', 'response_format'])
+ ->and($decoded['model'])->toBe('deepseek-v4-pro')
+ ->and($decoded['temperature'])->toBe(0.5)
+ ->and($decoded['max_tokens'])->toBe(1024);
+});