From 8957aceaf1267212c8fb87c4f224c6d9b4b2c2f8 Mon Sep 17 00:00:00 2001 From: omaralalwi Date: Fri, 22 May 2026 03:42:53 +0300 Subject: [PATCH 1/4] =?UTF-8?q?Release=20v2.1.0=20=E2=80=94=20Foundation?= =?UTF-8?q?=20+=20Bug=20Fixes=20(V4=20models,=20base=20URL=20fix,=20keep-a?= =?UTF-8?q?live=20stripping)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 128 +++++++++++- README.md | 32 ++- TODO.md | 299 +++++++++++++++++++++++++++ composer.json | 2 +- docs/FUNCTION-CALLING.md | 13 +- src/Enums/Configs/DefaultConfigs.php | 2 +- src/Enums/Models.php | 40 ++++ src/Models/ResultAbstract.php | 22 +- src/Traits/Resources/HasChat.php | 18 +- src/Traits/Resources/HasCoder.php | 18 +- tests/Feature/V210ChangesTest.php | 146 +++++++++++++ 11 files changed, 693 insertions(+), 27 deletions(-) create mode 100644 TODO.md create mode 100644 tests/Feature/V210ChangesTest.php 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/TODO.md b/TODO.md new file mode 100644 index 0000000..c77826c --- /dev/null +++ b/TODO.md @@ -0,0 +1,299 @@ +# TODO: Missing DeepSeek API Features in PHP Client + +> Generated by comparing the [DeepSeek API Docs](https://api-docs.deepseek.com/) against the current `deepseek-php/deepseek-php-client` v2.0.0.6 implementation. + +--- + +## Backward Compatibility Policy + +This package is used in 100k+ production installs. Implementers MUST classify every change below before merging, and respect the v2.x line's no-breaking-changes commitment (see [CHANGELOG.md](CHANGELOG.md)). + +Every item carries one of three tags: + +- **`[BC-safe]`** — Pure addition: new methods, new optional parameters with defaults, new enum cases, new endpoint resources, new opt-in client variants. Ships in `v2.1.x`. +- **`[Behavior]`** — Fixes an existing bug or makes a previously-silent feature work. Request body or response shape may differ, but no public signature changes. Ships in `v2.1.x` with a prominent changelog note. +- **`[BREAKING]`** — Removes/renames public API (methods, classes, traits, enum cases), changes return types, mutates defaults that change cost or observable behavior, or expands a published interface. **Must wait for `v3.0.0`** and ship with a migration guide. + +Deprecation pattern for items tagged `[BREAKING]`: + +1. In `v2.1.x`, add `@deprecated` docblock annotations and a `trigger_error(..., E_USER_DEPRECATED)` on use. +2. Keep the symbol fully functional throughout the `v2.x` line. +3. Remove only in `v3.0.0`, with an entry in [MIGRATION.md](MIGRATION.md). + +--- + +## P0 - Critical / Breaking Changes + +### 1. **[DONE in v2.1.0]** Base URL is wrong  `[Behavior]` +- ~~**Current**: `https://api.deepseek.com/v3` (`DefaultConfigs::BASE_URL`)~~ +- ~~**API Docs**: `https://api.deepseek.com` (no `/v3` suffix)~~ +- ~~**Impact**: The `/v3` base URL may stop working or already routes incorrectly for newer endpoints (Beta features, user balance, etc.)~~ +- ~~**BC note**: `/v3` is already wrong; fixing it is a bug fix. Anyone overriding `baseUrl` is unaffected. Safe for `v2.1.x` with a changelog entry under `Fixed`.~~ +- **Resolution**: `DefaultConfigs::BASE_URL` corrected to `https://api.deepseek.com` in v2.1.0. + +### 2. Models enum is outdated / missing current models  `[BC-safe]` (additions) + `[BREAKING]` (removals) + +**[DONE in v2.1.0]** for the `[BC-safe]` additions and `@deprecated` annotations. Removal of legacy cases is reserved for v3.0.0. + +- ~~**Missing**:~~ + - ~~`deepseek-v4-pro` (flagship model: 1M context, thinking/non-thinking, MAX OUTPUT 384K)~~ — added as `Models::V4_PRO` in v2.1.0 + - ~~`deepseek-v4-flash` (fast/efficient: 1M context, thinking/non-thinking, MAX OUTPUT 384K)~~ — added as `Models::V4_FLASH` in v2.1.0 +- ~~Current `Models` enum values: `deepseek-chat`, `deepseek-coder`, `DeepSeek-R1`, `DeepSeek-R1-Zero`~~ — all four marked `@deprecated` in v2.1.0 (still fully functional). +- **Still pending for v3.0.0**: removal of the four deprecated cases. +- **Still pending for v2.2.0** (item 22): `DefaultConfigs::MODEL = 'DeepSeek-R1'` vs runtime `$model = null` inconsistency. +- **Reasoning model token limits**: `max_tokens` default is 32K, maximum 64K when thinking mode is enabled (per [reasoning_model docs](https://api-docs.deepseek.com/guides/reasoning_model)). Cross-reference item 23. + +### 3. Streaming is not actually implemented  `[BC-safe]` +- `withStream()` sets `stream: true` in the request body, but `Resource::sendRequest()` reads the entire response body synchronously via `getContents()` — SSE chunks are never parsed. +- `stream_options.include_usage` is not exposed. +- Users expecting real-time token streaming will get a raw, unparsed SSE string. +- **BC note**: Add a new `runStreamed(callable $onChunk): void` method instead of mutating `run()`'s `string` return type. Existing `->withStream()->run()` callers continue to receive a raw string (matching current behavior). + +--- + +## P1 - High Priority (Missing API Parameters) + +### 4. Thinking Mode (`thinking` + `reasoning_effort`)  `[BC-safe]` +**API support**: `thinking: {type: "enabled"|"disabled"}` and `reasoning_effort: "high"|"max"` + +- No `setThinkingMode(bool $enabled)` or `setThinking(array $config)` method +- No `setReasoningEffort(string $effort)` method +- `reasoning_content` in the response is never parsed or exposed to the user +- Tool call turns in thinking mode require `reasoning_content` to be passed back — impossible to do correctly without this +- **Defaults**: `thinking.type` defaults to `enabled`, `reasoning_effort` defaults to `high` for normal requests and `max` for agent flows like Claude Code / OpenCode. +- **Caveat**: In thinking mode, `temperature` / `top_p` are silently ignored, and `logprobs` / `top_logprobs` return HTTP 400 (per [reasoning_model docs](https://api-docs.deepseek.com/guides/reasoning_model)). The current client always sends `temperature` and would always fail logprobs requests in thinking mode. +- **BC note**: New setters with default = null (omitted from request body when unset). + +### 5. `stop` parameter  `[BC-safe]` +**API support**: Up to 16 stop sequences (string or string[]) + +- No `setStop(string|array $stop)` method +- Not included in `QueryFlags` enum + +### 6. `top_p` parameter  `[BC-safe]` +**API support**: Nucleus sampling (0–1) + +- No `setTopP(float $topP)` method +- Not included in `QueryFlags` enum + +### 7. `tool_choice` parameter  `[BC-safe]` +**API support**: `"none"`, `"auto"`, `"required"`, or `{"type": "function", "function": {"name": "..."}}` + +- No method to set tool_choice despite `setTools()` existing +- `"required"` mode (force function calling) is missing +- Named tool choice is missing + +### 8. `logprobs` and `top_logprobs`  `[BC-safe]` +**API support**: Return log probabilities of output tokens + +- No `setLogprobs(bool)` or `setTopLogprobs(int)` methods +- Response parsing: logprobs not extracted from response JSON + +### 9. `user_id` parameter  `[BC-safe]` +**API support**: Rate limit isolation, content safety, KVCache isolation + +- No `setUserId(string $userId)` method +- Not included in `QueryFlags` enum + +### 10. `name` field on messages  `[BC-safe]` +**API support**: Optional `name` field on system/user/assistant messages to differentiate participants + +- `query()` and `buildQuery()` only accept `role` and `content` +- No way to pass `name` in message objects +- **BC note**: Add optional 3rd parameter `?string $name = null` to `query()` and `buildQuery()`. Existing 2-arg callers unaffected. + +### 11. Chat Prefix Completion (Beta)  `[BC-safe]` +**API support**: `prefix: true` on assistant message, `reasoning_content` on last assistant message, requires `base_url=https://api.deepseek.com/beta` + +- No way to set `prefix: true` on assistant messages +- No way to pass `reasoning_content` to assistant messages +- No support for the `/beta` base URL variant +- Example use case: force model to output Python code with a prefix ` ```python\n` +- **Cross-reference**: Also reachable via Anthropic format (item 16b) using `array, type = "thinking"` blocks. + +### 12. `stop` / `stream_options.include_usage` for streaming  `[BC-safe]` +- `stream_options` not exposed +- `include_usage` chunk not available +- **BC note**: Pairs with item 3. + +### 13. Structured tool call `strict` mode (Beta)  `[BC-safe]` +**API support**: `strict: true` on function definitions to enforce JSON schema compliance + +- Tool definitions passed via `setTools()` are sent as-is, but there's no builder/helper for `strict` mode +- **BC note**: Already supported by passing raw arrays via `setTools()`; just add a fluent helper. + +### 13b. `Coder` resource and `HasCoder` trait are dead code  `[BREAKING]` +`deepseek-coder` no longer exists in the DeepSeek API. The following symbols reference a removed model: + +- [`src/Resources/Coder.php`](src/Resources/Coder.php) +- [`src/Traits/Resources/HasCoder.php`](src/Traits/Resources/HasCoder.php) (`code()` method) +- [`Models::CODER`](src/Enums/Models.php) + +Per the [pricing docs](https://api-docs.deepseek.com/quick_start/pricing), only `deepseek-v4-flash` and `deepseek-v4-pro` exist in the API (with `deepseek-chat` / `deepseek-reasoner` as deprecated aliases retiring 2026-07-24). + +- **BC note**: Removing `code()`, `HasCoder`, and `Coder::class` is `[BREAKING]`. In `v2.1.x` mark `@deprecated` and have `code()` route to `chat()`. Delete only in `v3.0.0`. + +### 13c. **[DONE in v2.1.0]** Request Keep-Alive lines not stripped  `[Behavior]` +~~Per the [Rate Limit docs](https://api-docs.deepseek.com/quick_start/rate_limit), the API sends content while waiting for inference to start:~~ + +- ~~**Non-streaming**: Continuously returns empty lines until JSON body arrives.~~ +- ~~**Streaming**: Continuously returns SSE comments `: keep-alive`.~~ +- ~~The server closes idle connections after 10 minutes if inference never starts.~~ + +~~[`Resource::sendRequest()`](src/Resources/Resource.php) does not filter these. The raw response may contain leading blank lines or `: keep-alive` comments that break naive `json_decode()` and SSE parsing.~~ + +- **Resolution**: [`ResultAbstract::setContent()`](src/Models/ResultAbstract.php) now strips leading empty lines and `: keep-alive` SSE comments in v2.1.0. Both `SuccessResult` and `BadResult` benefit automatically (`FailureResult` constructed via `new FailureResult($code, $message)` bypasses `setContent()` and is unaffected). + +--- + +## P2 - Medium Priority (Missing Endpoints & Features) + +### 14. User Balance API (`GET /user/balance`)  `[BC-safe]` +**API support**: Returns balance info (`is_available`, `balance_infos` with currency, total/granted/topped-up balance) + +- No `getUserBalance()` method +- No `EndpointSuffixes::USER_BALANCE` enum value +- No response DTO for balance info + +### 15. FIM Completion API (`POST /completions`) — Beta  `[BC-safe]` +**API support**: Fill-in-the-Middle completion with `prompt` + `suffix`, max 4K tokens, requires `base_url=https://api.deepseek.com/beta` + +- Entire `/completions` endpoint not implemented +- No `EndpointSuffixes::COMPLETIONS` enum value +- No resource class for FIM completions +- Parameters needed: `prompt`, `suffix`, `max_tokens`, `temperature`, `top_p`, `stop`, `stream`, `logprobs`, `echo` + +### 15b. HTTP 429 / rate-limit handling  `[BC-safe]` +No dedicated handling for HTTP 429 (concurrency limit exceeded). [`Resource::sendRequest()`](src/Resources/Resource.php) only routes to generic `BadResult` / `FailureResult`. + +Per [Rate Limit docs](https://api-docs.deepseek.com/quick_start/rate_limit): +- `deepseek-v4-pro`: 500 concurrent requests per account +- `deepseek-v4-flash`: 2500 concurrent requests per account +- When `user_id` isolation is active, per-user limits apply on top of the account-wide cap + +Should add: `RateLimitResult` class with parsed `Retry-After` header, and optional exponential-backoff helper. + +- **BC note**: New result class; existing `BadResult` / `FailureResult` callers keep working since 429s currently fall through to one of those. + +### 16. Anthropic API format support  `[BC-safe]` +**API support**: `base_url=https://api.deepseek.com/anthropic` with full Anthropic Messages API compatibility + +- No Anthropic-format client variant +- No `ClientTypes::ANTHROPIC` enum value +- **BC note**: Opt-in via `build(..., clientType: 'anthropic')`. Default OpenAI-format behavior unchanged. + +### 16b. Anthropic message-block builders  `[BC-safe]` +Item 16 only mentions the `base_url` / client variant. Per the [Anthropic API compatibility table](https://api-docs.deepseek.com/guides/anthropic_api), full support also requires: + +- `content` as an array of typed blocks: `type=text|thinking|tool_use|tool_result` + - Not supported by DeepSeek's bridge: `image`, `document`, `search_result`, `web_search_tool_result`, `code_execution_tool_result`, `mcp_tool_use`, `mcp_tool_result`, `container_upload`, `server_tool_use`, `redacted_thinking` +- Top-level `system` field (not a `system`-role message) +- `metadata.user_id` instead of top-level `user_id` +- `tool_choice` shape differs: `{type: "any"|"auto"|"tool"|"none"}` vs OpenAI's `none`/`auto`/`required` +- `stop_sequences` instead of `stop` +- `output_config.effort` is the Anthropic equivalent of `reasoning_effort` + +[`QueryRoles`](src/Enums/Queries/QueryRoles.php) and [`HasToolsFunctionCalling`](src/Traits/Client/HasToolsFunctionCalling.php) are OpenAI-shape only. Add separate Anthropic builders. + +--- + +## P3 - Lower Priority (Developer Experience & Response Parsing) + +### 17. No structured response DTO  `[BC-safe]` (additive) / `[BREAKING]` (if `run()` retyped) +- `run()` returns raw JSON string — users must `json_decode()` manually +- No parsed `choices` array, no `message.content` extraction, no `usage` breakdown +- Response fields not exposed: `finish_reason`, `reasoning_content`, `tool_calls`, `usage.prompt_cache_hit_tokens`, `usage.prompt_cache_miss_tokens`, `usage.completion_tokens_details.reasoning_tokens`, `system_fingerprint`, `created`, `id` +- **BC note**: Add a new accessor like `getResult()->getMessage()` / `getResult()->getUsage()` / new `runAsObject()` method. **Do not change `run()`'s `string` return type** — that's `[BREAKING]` and must wait for v3.0.0. + +### 18. No conversation/message history management  `[BC-safe]` +- `$queries` array is internal-only (no getter) +- No `getQueries()` or `getMessages()` method +- Cannot retrieve message history for multi-turn conversations +- Users must manually track their own message history + +### 19. No dedicated `setSystemMessage()` method  `[BC-safe]` +- Must use `query('You are a helpful assistant', 'system')` — works but not idiomatic +- A `setSystemPrompt(string $content)` or `withSystemMessage(string $content)` would match other SDK conventions +- **BC note**: Existing `query($content, 'system')` callers keep working. + +### 20. **[DONE in v2.1.0]** `chat()` and `code()` shortcuts ignore temperature/max_tokens/tools/response_format  `[Behavior]` +- ~~`HasChat::chat()` and `HasCoder::code()` only pass `messages`, `model`, `stream` to the resource~~ +- ~~Temperature, max_tokens, tools, response_format set on the client are silently ignored when using these shortcuts~~ +- ~~**BC note**: Fixing this is a bug fix. Request body will grow new fields to match what `run()` sends. Anyone snapshot-testing the request body will see new fields — flag prominently under `Fixed` in the changelog.~~ +- **Resolution**: Both [`HasChat::chat()`](src/Traits/Resources/HasChat.php) and [`HasCoder::code()`](src/Traits/Resources/HasCoder.php) now build the same payload as [`DeepSeekClient::run()`](src/DeepSeekClient.php) in v2.1.0. + +### 21. `ClientContract` interface out of sync with implementation  `[BREAKING]` +- Missing declarations for: `resetQueries()`, `setTemperature()`, `setMaxTokens()`, `setResponseFormat()`, `setResult()`, `getResult()` +- `build()` signature in contract missing 4th `$clientType` parameter +- **BC note**: Adding methods to a published interface is `[BREAKING]` for any user implementing / mocking / decorating it. Options: + - **(a) Defer to v3.0.0** (preferred) — expand `ClientContract` to match implementation. + - **(b) v2.1.x compromise** — introduce a new `ExtendedClientContract` interface that `DeepSeekClient` implements alongside the existing `ClientContract`, leaving `ClientContract` untouched. + +### 22. `DefaultConfigs::MODEL` and `TemperatureValues` are inconsistent  `[BC-safe]` (fix) / `[BREAKING]` (cleanup) +- Declared as `'DeepSeek-R1'` but runtime `$model = null` and README says `deepseek-chat` +- `TemperatureValues` enum mixes temperature presets with non-temperature defaults (`MAX_TOKENS`, `RESPONSE_FORMAT_TYPE`) +- **BC note**: Fixing the unused `MODEL` constant is `[BC-safe]`. Removing `TemperatureValues::MAX_TOKENS` / `RESPONSE_FORMAT_TYPE` is `[BREAKING]` — defer to v3.0.0; add new `DefaultConfigs::MAX_TOKENS` and `DefaultConfigs::RESPONSE_FORMAT_TYPE` in v2.1.x alongside the deprecated cases. + +### 23. Default `MAX_TOKENS = 4096` is low for V4 models  `[BREAKING]` +- V4-Flash / V4-Pro both support 1M context and max **384K** output tokens +- Default 4096 is only ~1% of the model's capacity +- **BC note**: Raising the default silently increases user costs. Must wait for v3.0.0 with a prominent migration note. In v2.x, document that users should call `setMaxTokens()` explicitly for V4 models. + +### 24. No `getQueries()` / `getConfig()` introspection  `[BC-safe]` +- No way to inspect current client state (model, temperature, max_tokens, tools, etc.) +- Debugging requires dumping internals manually + +### 25. Fluent chain order sensitivity / no guards  `[BC-safe]` +- `getModelsList()` switches endpoint and method — calling `run()` afterward sends GET to `/models`. Calling `query()` then `getModelsList()` then `run()` has undefined behavior. +- `setTools()` must be called before `run()` — no validation +- No `reset()` to return client to initial state (only `resetQueries()` exists) +- **BC note**: New `reset()` method is purely additive; existing `resetQueries()` keeps working. + +### 26. **[DONE in v2.1.0]** README.md is outdated  `[BC-safe]` +~~Doc-only refresh of [README.md](README.md):~~ + +- ~~[`README.md:95`](README.md) advertises `baseUrl:'https://api.deepseek.com/v3'` (wrong — per docs it should be `https://api.deepseek.com`).~~ +- ~~[`README.md:98`](README.md) uses `Models::CODER->value` example (model no longer exists in the API).~~ +- ~~[`README.md:86`](README.md) says default temperature is `0.8`, but [`DeepSeekClient::__construct`](src/DeepSeekClient.php) sets it to `TemperatureValues::GENERAL_CONVERSATION = 1.3`.~~ +- ~~[`README.md:167`](README.md) shows `getModelsList()` example output listing `deepseek-chat` + `deepseek-reasoner` only (no V4 models).~~ +- **Partially pending**: mention of thinking mode and the Anthropic endpoint deferred to v2.2.0 (thinking mode setters) and beyond. v2.1.0 refresh added the V4-models callout and 1M context note. +- **Resolution**: README updated in v2.1.0 — temperature default corrected to 1.3, base URL examples drop `/v3`, advanced example now uses `Models::V4_PRO`, `getModelsList()` output now lists V4 models, new "Supported Models" callout added under Features. + +### 27. **[DONE in v2.1.0]** `docs/FUNCTION-CALLING.md` is outdated  `[BC-safe]` +~~Doc-only refresh of [docs/FUNCTION-CALLING.md](docs/FUNCTION-CALLING.md):~~ + +- ~~Lines 56, 139, 179 use `deepseek-chat` in JSON examples — should be `deepseek-v4-pro` / `deepseek-v4-flash`.~~ +- ~~No mention that thinking mode requires `reasoning_content` to be echoed back on tool turns (otherwise the next request 400s).~~ +- **Still pending for v2.2.0**: documentation of `tool_choice` (item 7) and `strict` mode (item 13) — both ship as new code in v2.2.0. +- **Resolution**: JSON examples now use `deepseek-v4-pro`, and a new "Thinking-mode caveat" section was added explaining the `reasoning_content` echo requirement. + +### 28. `Models::R1Zero` is invalid  `[BREAKING]` +- [`Models::R1Zero = 'DeepSeek-R1-Zero'`](src/Enums/Models.php) — this name was never a valid DeepSeek API model id. Subset of item 2, separated for clarity: this case should simply be deleted (not migrated to a V4 equivalent). +- **BC note**: Mark `@deprecated` in v2.1.x; remove only in v3.0.0. + +--- + +## Summary + +| Priority | BC-safe | Behavior | Breaking | Total | +|---|---|---|---|---| +| P0 - Critical/Breaking | 1 | 1 | 1\* | 3 | +| P1 - High Priority | 9 | 1 | 1 | 11 | +| P2 - Medium Priority | 4 | 0 | 0 | 4 | +| P3 - Lower Priority | 8 | 1 | 5\* | 14 | +| **Total** | **22** | **3** | **7** | **32** | + +\* Items 2, 17, and 22 are dual-tagged (BC-safe additive portion + Breaking removal portion). Counted once under `Breaking` in the totals. + +### Progress + +| Release | Items delivered | Items remaining | +|---|---|---| +| **v2.1.0 (released)** | #1, #2 (additions + deprecations), #13c, #20, #26, #27 — **6 items** | 19 BC-safe / Behavior + 7 Breaking | +| **v2.2.0 (planned)** | #4, #5, #6, #7, #8, #9, #10, #13, #17 (additive portion), #18, #19, #22 (additive portion), #24, #25, plus `ExtendedClientContract` for #21 — **~14 items** | TBD | +| **v2.3.0 (planned)** | #3, #11, #12, #14, #15, #15b — **6 items** | Anthropic items (#16, #16b) deferred indefinitely per user direction | +| **v3.0.0 (no ETA)** | All `[BREAKING]` items (#2 removals, #13b removal, #17 retype, #21 expansion, #22 cleanup, #23, #28) | — | + +**Release strategy:** +- **`v2.1.x`** — All `[BC-safe]` (22) + `[Behavior]` (3) items = **25 deliverables**, zero breakages. +- **`v3.0.0`** — All `[BREAKING]` (7) items + the migration guide in [MIGRATION.md](MIGRATION.md). diff --git a/composer.json b/composer.json index e9e0a67..078d3ed 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "role": "creator" } ], - "version": "2.0.6", + "version": "2.1.0", "require": { "php": "^8.1.0", "nyholm/psr7": "^1.8", 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/src/Enums/Configs/DefaultConfigs.php b/src/Enums/Configs/DefaultConfigs.php index 204c9d1..9b7717b 100644 --- a/src/Enums/Configs/DefaultConfigs.php +++ b/src/Enums/Configs/DefaultConfigs.php @@ -4,7 +4,7 @@ enum DefaultConfigs: string { - case BASE_URL = 'https://api.deepseek.com/v3'; + case BASE_URL = 'https://api.deepseek.com'; case MODEL = 'DeepSeek-R1'; case TIMEOUT = '30'; case STREAM = 'false'; diff --git a/src/Enums/Models.php b/src/Enums/Models.php index ff699ed..f6a7248 100644 --- a/src/Enums/Models.php +++ b/src/Enums/Models.php @@ -4,8 +4,48 @@ 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-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/Models/ResultAbstract.php b/src/Models/ResultAbstract.php index a5b121a..26e76fe 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) { $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/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/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); +}); From 978eb541221414a4cea0f67cb177a4be2fffb1a7 Mon Sep 17 00:00:00 2001 From: omaralalwi Date: Sun, 24 May 2026 03:33:13 +0300 Subject: [PATCH 2/4] fix bugs in tests --- src/Contracts/ClientContract.php | 8 +- .../Factories/ApiFactoryContract.php | 13 +- src/Contracts/Models/ResultContract.php | 3 - src/Contracts/Resources/ResourceContract.php | 6 - src/DeepSeekClient.php | 76 +++---- src/Enums/Configs/DefaultConfigs.php | 2 +- src/Enums/Configs/TemperatureValues.php | 51 ++++- src/Enums/Models.php | 8 + src/Factories/ApiFactory.php | 30 ++- src/Models/BadResult.php | 6 +- src/Models/FailureResult.php | 6 +- src/Models/ResultAbstract.php | 2 +- src/Models/SuccessResult.php | 6 +- src/Resources/Chat.php | 5 +- src/Resources/Coder.php | 2 +- src/Resources/Resource.php | 23 +- src/Traits/Client/HasToolsFunctionCalling.php | 16 +- src/Traits/Queries/HasQueryParams.php | 28 +-- .../Feature/ClientDependency/FakeResponse.php | 9 +- tests/Feature/DeepSeekClientTest.php | 59 +++-- tests/Feature/FunctionCallingTest.php | 213 ++++++++++-------- 21 files changed, 324 insertions(+), 248 deletions(-) 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 9b7717b..cd4a907 100644 --- a/src/Enums/Configs/DefaultConfigs.php +++ b/src/Enums/Configs/DefaultConfigs.php @@ -5,7 +5,7 @@ enum DefaultConfigs: string { case BASE_URL = 'https://api.deepseek.com'; - case MODEL = 'DeepSeek-R1'; + 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 f6a7248..4cbcec3 100644 --- a/src/Enums/Models.php +++ b/src/Enums/Models.php @@ -12,6 +12,14 @@ enum Models: string */ 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. 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 26e76fe..2afc6e4 100644 --- a/src/Models/ResultAbstract.php +++ b/src/Models/ResultAbstract.php @@ -25,7 +25,7 @@ public function __construct(?int $statusCode = null, ?string $content = null) $this->content = $content; } - protected function setStatusCode(int $statusCode) + protected function setStatusCode(int $statusCode): void { $this->statusCode = $statusCode; } 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/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); }); From 2fd2084450d0c90b1b1ecd56fdb133c140144cd5 Mon Sep 17 00:00:00 2001 From: Omar Alalwi Date: Sun, 24 May 2026 03:37:12 +0300 Subject: [PATCH 3/4] Delete TODO.md --- TODO.md | 299 -------------------------------------------------------- 1 file changed, 299 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index c77826c..0000000 --- a/TODO.md +++ /dev/null @@ -1,299 +0,0 @@ -# TODO: Missing DeepSeek API Features in PHP Client - -> Generated by comparing the [DeepSeek API Docs](https://api-docs.deepseek.com/) against the current `deepseek-php/deepseek-php-client` v2.0.0.6 implementation. - ---- - -## Backward Compatibility Policy - -This package is used in 100k+ production installs. Implementers MUST classify every change below before merging, and respect the v2.x line's no-breaking-changes commitment (see [CHANGELOG.md](CHANGELOG.md)). - -Every item carries one of three tags: - -- **`[BC-safe]`** — Pure addition: new methods, new optional parameters with defaults, new enum cases, new endpoint resources, new opt-in client variants. Ships in `v2.1.x`. -- **`[Behavior]`** — Fixes an existing bug or makes a previously-silent feature work. Request body or response shape may differ, but no public signature changes. Ships in `v2.1.x` with a prominent changelog note. -- **`[BREAKING]`** — Removes/renames public API (methods, classes, traits, enum cases), changes return types, mutates defaults that change cost or observable behavior, or expands a published interface. **Must wait for `v3.0.0`** and ship with a migration guide. - -Deprecation pattern for items tagged `[BREAKING]`: - -1. In `v2.1.x`, add `@deprecated` docblock annotations and a `trigger_error(..., E_USER_DEPRECATED)` on use. -2. Keep the symbol fully functional throughout the `v2.x` line. -3. Remove only in `v3.0.0`, with an entry in [MIGRATION.md](MIGRATION.md). - ---- - -## P0 - Critical / Breaking Changes - -### 1. **[DONE in v2.1.0]** Base URL is wrong  `[Behavior]` -- ~~**Current**: `https://api.deepseek.com/v3` (`DefaultConfigs::BASE_URL`)~~ -- ~~**API Docs**: `https://api.deepseek.com` (no `/v3` suffix)~~ -- ~~**Impact**: The `/v3` base URL may stop working or already routes incorrectly for newer endpoints (Beta features, user balance, etc.)~~ -- ~~**BC note**: `/v3` is already wrong; fixing it is a bug fix. Anyone overriding `baseUrl` is unaffected. Safe for `v2.1.x` with a changelog entry under `Fixed`.~~ -- **Resolution**: `DefaultConfigs::BASE_URL` corrected to `https://api.deepseek.com` in v2.1.0. - -### 2. Models enum is outdated / missing current models  `[BC-safe]` (additions) + `[BREAKING]` (removals) - -**[DONE in v2.1.0]** for the `[BC-safe]` additions and `@deprecated` annotations. Removal of legacy cases is reserved for v3.0.0. - -- ~~**Missing**:~~ - - ~~`deepseek-v4-pro` (flagship model: 1M context, thinking/non-thinking, MAX OUTPUT 384K)~~ — added as `Models::V4_PRO` in v2.1.0 - - ~~`deepseek-v4-flash` (fast/efficient: 1M context, thinking/non-thinking, MAX OUTPUT 384K)~~ — added as `Models::V4_FLASH` in v2.1.0 -- ~~Current `Models` enum values: `deepseek-chat`, `deepseek-coder`, `DeepSeek-R1`, `DeepSeek-R1-Zero`~~ — all four marked `@deprecated` in v2.1.0 (still fully functional). -- **Still pending for v3.0.0**: removal of the four deprecated cases. -- **Still pending for v2.2.0** (item 22): `DefaultConfigs::MODEL = 'DeepSeek-R1'` vs runtime `$model = null` inconsistency. -- **Reasoning model token limits**: `max_tokens` default is 32K, maximum 64K when thinking mode is enabled (per [reasoning_model docs](https://api-docs.deepseek.com/guides/reasoning_model)). Cross-reference item 23. - -### 3. Streaming is not actually implemented  `[BC-safe]` -- `withStream()` sets `stream: true` in the request body, but `Resource::sendRequest()` reads the entire response body synchronously via `getContents()` — SSE chunks are never parsed. -- `stream_options.include_usage` is not exposed. -- Users expecting real-time token streaming will get a raw, unparsed SSE string. -- **BC note**: Add a new `runStreamed(callable $onChunk): void` method instead of mutating `run()`'s `string` return type. Existing `->withStream()->run()` callers continue to receive a raw string (matching current behavior). - ---- - -## P1 - High Priority (Missing API Parameters) - -### 4. Thinking Mode (`thinking` + `reasoning_effort`)  `[BC-safe]` -**API support**: `thinking: {type: "enabled"|"disabled"}` and `reasoning_effort: "high"|"max"` - -- No `setThinkingMode(bool $enabled)` or `setThinking(array $config)` method -- No `setReasoningEffort(string $effort)` method -- `reasoning_content` in the response is never parsed or exposed to the user -- Tool call turns in thinking mode require `reasoning_content` to be passed back — impossible to do correctly without this -- **Defaults**: `thinking.type` defaults to `enabled`, `reasoning_effort` defaults to `high` for normal requests and `max` for agent flows like Claude Code / OpenCode. -- **Caveat**: In thinking mode, `temperature` / `top_p` are silently ignored, and `logprobs` / `top_logprobs` return HTTP 400 (per [reasoning_model docs](https://api-docs.deepseek.com/guides/reasoning_model)). The current client always sends `temperature` and would always fail logprobs requests in thinking mode. -- **BC note**: New setters with default = null (omitted from request body when unset). - -### 5. `stop` parameter  `[BC-safe]` -**API support**: Up to 16 stop sequences (string or string[]) - -- No `setStop(string|array $stop)` method -- Not included in `QueryFlags` enum - -### 6. `top_p` parameter  `[BC-safe]` -**API support**: Nucleus sampling (0–1) - -- No `setTopP(float $topP)` method -- Not included in `QueryFlags` enum - -### 7. `tool_choice` parameter  `[BC-safe]` -**API support**: `"none"`, `"auto"`, `"required"`, or `{"type": "function", "function": {"name": "..."}}` - -- No method to set tool_choice despite `setTools()` existing -- `"required"` mode (force function calling) is missing -- Named tool choice is missing - -### 8. `logprobs` and `top_logprobs`  `[BC-safe]` -**API support**: Return log probabilities of output tokens - -- No `setLogprobs(bool)` or `setTopLogprobs(int)` methods -- Response parsing: logprobs not extracted from response JSON - -### 9. `user_id` parameter  `[BC-safe]` -**API support**: Rate limit isolation, content safety, KVCache isolation - -- No `setUserId(string $userId)` method -- Not included in `QueryFlags` enum - -### 10. `name` field on messages  `[BC-safe]` -**API support**: Optional `name` field on system/user/assistant messages to differentiate participants - -- `query()` and `buildQuery()` only accept `role` and `content` -- No way to pass `name` in message objects -- **BC note**: Add optional 3rd parameter `?string $name = null` to `query()` and `buildQuery()`. Existing 2-arg callers unaffected. - -### 11. Chat Prefix Completion (Beta)  `[BC-safe]` -**API support**: `prefix: true` on assistant message, `reasoning_content` on last assistant message, requires `base_url=https://api.deepseek.com/beta` - -- No way to set `prefix: true` on assistant messages -- No way to pass `reasoning_content` to assistant messages -- No support for the `/beta` base URL variant -- Example use case: force model to output Python code with a prefix ` ```python\n` -- **Cross-reference**: Also reachable via Anthropic format (item 16b) using `array, type = "thinking"` blocks. - -### 12. `stop` / `stream_options.include_usage` for streaming  `[BC-safe]` -- `stream_options` not exposed -- `include_usage` chunk not available -- **BC note**: Pairs with item 3. - -### 13. Structured tool call `strict` mode (Beta)  `[BC-safe]` -**API support**: `strict: true` on function definitions to enforce JSON schema compliance - -- Tool definitions passed via `setTools()` are sent as-is, but there's no builder/helper for `strict` mode -- **BC note**: Already supported by passing raw arrays via `setTools()`; just add a fluent helper. - -### 13b. `Coder` resource and `HasCoder` trait are dead code  `[BREAKING]` -`deepseek-coder` no longer exists in the DeepSeek API. The following symbols reference a removed model: - -- [`src/Resources/Coder.php`](src/Resources/Coder.php) -- [`src/Traits/Resources/HasCoder.php`](src/Traits/Resources/HasCoder.php) (`code()` method) -- [`Models::CODER`](src/Enums/Models.php) - -Per the [pricing docs](https://api-docs.deepseek.com/quick_start/pricing), only `deepseek-v4-flash` and `deepseek-v4-pro` exist in the API (with `deepseek-chat` / `deepseek-reasoner` as deprecated aliases retiring 2026-07-24). - -- **BC note**: Removing `code()`, `HasCoder`, and `Coder::class` is `[BREAKING]`. In `v2.1.x` mark `@deprecated` and have `code()` route to `chat()`. Delete only in `v3.0.0`. - -### 13c. **[DONE in v2.1.0]** Request Keep-Alive lines not stripped  `[Behavior]` -~~Per the [Rate Limit docs](https://api-docs.deepseek.com/quick_start/rate_limit), the API sends content while waiting for inference to start:~~ - -- ~~**Non-streaming**: Continuously returns empty lines until JSON body arrives.~~ -- ~~**Streaming**: Continuously returns SSE comments `: keep-alive`.~~ -- ~~The server closes idle connections after 10 minutes if inference never starts.~~ - -~~[`Resource::sendRequest()`](src/Resources/Resource.php) does not filter these. The raw response may contain leading blank lines or `: keep-alive` comments that break naive `json_decode()` and SSE parsing.~~ - -- **Resolution**: [`ResultAbstract::setContent()`](src/Models/ResultAbstract.php) now strips leading empty lines and `: keep-alive` SSE comments in v2.1.0. Both `SuccessResult` and `BadResult` benefit automatically (`FailureResult` constructed via `new FailureResult($code, $message)` bypasses `setContent()` and is unaffected). - ---- - -## P2 - Medium Priority (Missing Endpoints & Features) - -### 14. User Balance API (`GET /user/balance`)  `[BC-safe]` -**API support**: Returns balance info (`is_available`, `balance_infos` with currency, total/granted/topped-up balance) - -- No `getUserBalance()` method -- No `EndpointSuffixes::USER_BALANCE` enum value -- No response DTO for balance info - -### 15. FIM Completion API (`POST /completions`) — Beta  `[BC-safe]` -**API support**: Fill-in-the-Middle completion with `prompt` + `suffix`, max 4K tokens, requires `base_url=https://api.deepseek.com/beta` - -- Entire `/completions` endpoint not implemented -- No `EndpointSuffixes::COMPLETIONS` enum value -- No resource class for FIM completions -- Parameters needed: `prompt`, `suffix`, `max_tokens`, `temperature`, `top_p`, `stop`, `stream`, `logprobs`, `echo` - -### 15b. HTTP 429 / rate-limit handling  `[BC-safe]` -No dedicated handling for HTTP 429 (concurrency limit exceeded). [`Resource::sendRequest()`](src/Resources/Resource.php) only routes to generic `BadResult` / `FailureResult`. - -Per [Rate Limit docs](https://api-docs.deepseek.com/quick_start/rate_limit): -- `deepseek-v4-pro`: 500 concurrent requests per account -- `deepseek-v4-flash`: 2500 concurrent requests per account -- When `user_id` isolation is active, per-user limits apply on top of the account-wide cap - -Should add: `RateLimitResult` class with parsed `Retry-After` header, and optional exponential-backoff helper. - -- **BC note**: New result class; existing `BadResult` / `FailureResult` callers keep working since 429s currently fall through to one of those. - -### 16. Anthropic API format support  `[BC-safe]` -**API support**: `base_url=https://api.deepseek.com/anthropic` with full Anthropic Messages API compatibility - -- No Anthropic-format client variant -- No `ClientTypes::ANTHROPIC` enum value -- **BC note**: Opt-in via `build(..., clientType: 'anthropic')`. Default OpenAI-format behavior unchanged. - -### 16b. Anthropic message-block builders  `[BC-safe]` -Item 16 only mentions the `base_url` / client variant. Per the [Anthropic API compatibility table](https://api-docs.deepseek.com/guides/anthropic_api), full support also requires: - -- `content` as an array of typed blocks: `type=text|thinking|tool_use|tool_result` - - Not supported by DeepSeek's bridge: `image`, `document`, `search_result`, `web_search_tool_result`, `code_execution_tool_result`, `mcp_tool_use`, `mcp_tool_result`, `container_upload`, `server_tool_use`, `redacted_thinking` -- Top-level `system` field (not a `system`-role message) -- `metadata.user_id` instead of top-level `user_id` -- `tool_choice` shape differs: `{type: "any"|"auto"|"tool"|"none"}` vs OpenAI's `none`/`auto`/`required` -- `stop_sequences` instead of `stop` -- `output_config.effort` is the Anthropic equivalent of `reasoning_effort` - -[`QueryRoles`](src/Enums/Queries/QueryRoles.php) and [`HasToolsFunctionCalling`](src/Traits/Client/HasToolsFunctionCalling.php) are OpenAI-shape only. Add separate Anthropic builders. - ---- - -## P3 - Lower Priority (Developer Experience & Response Parsing) - -### 17. No structured response DTO  `[BC-safe]` (additive) / `[BREAKING]` (if `run()` retyped) -- `run()` returns raw JSON string — users must `json_decode()` manually -- No parsed `choices` array, no `message.content` extraction, no `usage` breakdown -- Response fields not exposed: `finish_reason`, `reasoning_content`, `tool_calls`, `usage.prompt_cache_hit_tokens`, `usage.prompt_cache_miss_tokens`, `usage.completion_tokens_details.reasoning_tokens`, `system_fingerprint`, `created`, `id` -- **BC note**: Add a new accessor like `getResult()->getMessage()` / `getResult()->getUsage()` / new `runAsObject()` method. **Do not change `run()`'s `string` return type** — that's `[BREAKING]` and must wait for v3.0.0. - -### 18. No conversation/message history management  `[BC-safe]` -- `$queries` array is internal-only (no getter) -- No `getQueries()` or `getMessages()` method -- Cannot retrieve message history for multi-turn conversations -- Users must manually track their own message history - -### 19. No dedicated `setSystemMessage()` method  `[BC-safe]` -- Must use `query('You are a helpful assistant', 'system')` — works but not idiomatic -- A `setSystemPrompt(string $content)` or `withSystemMessage(string $content)` would match other SDK conventions -- **BC note**: Existing `query($content, 'system')` callers keep working. - -### 20. **[DONE in v2.1.0]** `chat()` and `code()` shortcuts ignore temperature/max_tokens/tools/response_format  `[Behavior]` -- ~~`HasChat::chat()` and `HasCoder::code()` only pass `messages`, `model`, `stream` to the resource~~ -- ~~Temperature, max_tokens, tools, response_format set on the client are silently ignored when using these shortcuts~~ -- ~~**BC note**: Fixing this is a bug fix. Request body will grow new fields to match what `run()` sends. Anyone snapshot-testing the request body will see new fields — flag prominently under `Fixed` in the changelog.~~ -- **Resolution**: Both [`HasChat::chat()`](src/Traits/Resources/HasChat.php) and [`HasCoder::code()`](src/Traits/Resources/HasCoder.php) now build the same payload as [`DeepSeekClient::run()`](src/DeepSeekClient.php) in v2.1.0. - -### 21. `ClientContract` interface out of sync with implementation  `[BREAKING]` -- Missing declarations for: `resetQueries()`, `setTemperature()`, `setMaxTokens()`, `setResponseFormat()`, `setResult()`, `getResult()` -- `build()` signature in contract missing 4th `$clientType` parameter -- **BC note**: Adding methods to a published interface is `[BREAKING]` for any user implementing / mocking / decorating it. Options: - - **(a) Defer to v3.0.0** (preferred) — expand `ClientContract` to match implementation. - - **(b) v2.1.x compromise** — introduce a new `ExtendedClientContract` interface that `DeepSeekClient` implements alongside the existing `ClientContract`, leaving `ClientContract` untouched. - -### 22. `DefaultConfigs::MODEL` and `TemperatureValues` are inconsistent  `[BC-safe]` (fix) / `[BREAKING]` (cleanup) -- Declared as `'DeepSeek-R1'` but runtime `$model = null` and README says `deepseek-chat` -- `TemperatureValues` enum mixes temperature presets with non-temperature defaults (`MAX_TOKENS`, `RESPONSE_FORMAT_TYPE`) -- **BC note**: Fixing the unused `MODEL` constant is `[BC-safe]`. Removing `TemperatureValues::MAX_TOKENS` / `RESPONSE_FORMAT_TYPE` is `[BREAKING]` — defer to v3.0.0; add new `DefaultConfigs::MAX_TOKENS` and `DefaultConfigs::RESPONSE_FORMAT_TYPE` in v2.1.x alongside the deprecated cases. - -### 23. Default `MAX_TOKENS = 4096` is low for V4 models  `[BREAKING]` -- V4-Flash / V4-Pro both support 1M context and max **384K** output tokens -- Default 4096 is only ~1% of the model's capacity -- **BC note**: Raising the default silently increases user costs. Must wait for v3.0.0 with a prominent migration note. In v2.x, document that users should call `setMaxTokens()` explicitly for V4 models. - -### 24. No `getQueries()` / `getConfig()` introspection  `[BC-safe]` -- No way to inspect current client state (model, temperature, max_tokens, tools, etc.) -- Debugging requires dumping internals manually - -### 25. Fluent chain order sensitivity / no guards  `[BC-safe]` -- `getModelsList()` switches endpoint and method — calling `run()` afterward sends GET to `/models`. Calling `query()` then `getModelsList()` then `run()` has undefined behavior. -- `setTools()` must be called before `run()` — no validation -- No `reset()` to return client to initial state (only `resetQueries()` exists) -- **BC note**: New `reset()` method is purely additive; existing `resetQueries()` keeps working. - -### 26. **[DONE in v2.1.0]** README.md is outdated  `[BC-safe]` -~~Doc-only refresh of [README.md](README.md):~~ - -- ~~[`README.md:95`](README.md) advertises `baseUrl:'https://api.deepseek.com/v3'` (wrong — per docs it should be `https://api.deepseek.com`).~~ -- ~~[`README.md:98`](README.md) uses `Models::CODER->value` example (model no longer exists in the API).~~ -- ~~[`README.md:86`](README.md) says default temperature is `0.8`, but [`DeepSeekClient::__construct`](src/DeepSeekClient.php) sets it to `TemperatureValues::GENERAL_CONVERSATION = 1.3`.~~ -- ~~[`README.md:167`](README.md) shows `getModelsList()` example output listing `deepseek-chat` + `deepseek-reasoner` only (no V4 models).~~ -- **Partially pending**: mention of thinking mode and the Anthropic endpoint deferred to v2.2.0 (thinking mode setters) and beyond. v2.1.0 refresh added the V4-models callout and 1M context note. -- **Resolution**: README updated in v2.1.0 — temperature default corrected to 1.3, base URL examples drop `/v3`, advanced example now uses `Models::V4_PRO`, `getModelsList()` output now lists V4 models, new "Supported Models" callout added under Features. - -### 27. **[DONE in v2.1.0]** `docs/FUNCTION-CALLING.md` is outdated  `[BC-safe]` -~~Doc-only refresh of [docs/FUNCTION-CALLING.md](docs/FUNCTION-CALLING.md):~~ - -- ~~Lines 56, 139, 179 use `deepseek-chat` in JSON examples — should be `deepseek-v4-pro` / `deepseek-v4-flash`.~~ -- ~~No mention that thinking mode requires `reasoning_content` to be echoed back on tool turns (otherwise the next request 400s).~~ -- **Still pending for v2.2.0**: documentation of `tool_choice` (item 7) and `strict` mode (item 13) — both ship as new code in v2.2.0. -- **Resolution**: JSON examples now use `deepseek-v4-pro`, and a new "Thinking-mode caveat" section was added explaining the `reasoning_content` echo requirement. - -### 28. `Models::R1Zero` is invalid  `[BREAKING]` -- [`Models::R1Zero = 'DeepSeek-R1-Zero'`](src/Enums/Models.php) — this name was never a valid DeepSeek API model id. Subset of item 2, separated for clarity: this case should simply be deleted (not migrated to a V4 equivalent). -- **BC note**: Mark `@deprecated` in v2.1.x; remove only in v3.0.0. - ---- - -## Summary - -| Priority | BC-safe | Behavior | Breaking | Total | -|---|---|---|---|---| -| P0 - Critical/Breaking | 1 | 1 | 1\* | 3 | -| P1 - High Priority | 9 | 1 | 1 | 11 | -| P2 - Medium Priority | 4 | 0 | 0 | 4 | -| P3 - Lower Priority | 8 | 1 | 5\* | 14 | -| **Total** | **22** | **3** | **7** | **32** | - -\* Items 2, 17, and 22 are dual-tagged (BC-safe additive portion + Breaking removal portion). Counted once under `Breaking` in the totals. - -### Progress - -| Release | Items delivered | Items remaining | -|---|---|---| -| **v2.1.0 (released)** | #1, #2 (additions + deprecations), #13c, #20, #26, #27 — **6 items** | 19 BC-safe / Behavior + 7 Breaking | -| **v2.2.0 (planned)** | #4, #5, #6, #7, #8, #9, #10, #13, #17 (additive portion), #18, #19, #22 (additive portion), #24, #25, plus `ExtendedClientContract` for #21 — **~14 items** | TBD | -| **v2.3.0 (planned)** | #3, #11, #12, #14, #15, #15b — **6 items** | Anthropic items (#16, #16b) deferred indefinitely per user direction | -| **v3.0.0 (no ETA)** | All `[BREAKING]` items (#2 removals, #13b removal, #17 retype, #21 expansion, #22 cleanup, #23, #28) | — | - -**Release strategy:** -- **`v2.1.x`** — All `[BC-safe]` (22) + `[Behavior]` (3) items = **25 deliverables**, zero breakages. -- **`v3.0.0`** — All `[BREAKING]` (7) items + the migration guide in [MIGRATION.md](MIGRATION.md). From 458a5b7e03f2d22d14d7534c966026956d1e0ee0 Mon Sep 17 00:00:00 2001 From: omaralalwi Date: Sun, 24 May 2026 03:43:30 +0300 Subject: [PATCH 4/4] fix: resolve all CI failures and align codebase with DeepSeek API docs --- .../workflows/deepseek-automation-test.yml | 2 +- .gitignore | 3 --- composer.json | 2 +- phpstan.neon | 4 ++++ phpunit.xml | 19 +++++++++++++++++++ 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 phpstan.neon create mode 100644 phpunit.xml 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/composer.json b/composer.json index 078d3ed..626c9e4 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ ], "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/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 + + +