{"repo": "bramato/laravel-ai", "files": {"src/Clients/DeepSeekClient.php": {"fb44f12874a25c1a212c7b84771b5047eef1c77f": {"t": 1785746146.8893812, "n": 11414, "h": "83ad961ff03fb47f", "c": "httpClient = $this->configureHttpClient();\n // Determine the correct endpoint based on the base URI provided\n $this->apiEndpoint = $this->determineApiEndpoint();\n }\n\n /**\n * Configures the HTTP client with base URI and authentication.\n *\n * @return PendingRequest The configured HTTP client.\n */\n protected function configureHttpClient(): PendingRequest\n {\n // Default base URI for DeepSeek's OpenAI-compatible endpoint\n $baseUri = $this->options['base_uri'] ?? 'https://api.deepseek.com/v1';\n $timeout = $this->options['timeout'] ?? 30;\n\n return $this->httpFactory->baseUrl($baseUri)\n ->withToken($this->apiKey)\n ->acceptJson()\n ->asJson()\n ->timeout($timeout);\n }\n\n /**\n * Determines the correct relative API endpoint path based on the configured base URI.\n *\n * Handles cases where the base URI might or might not include the '/v1' path segment.\n *\n * @return string The API endpoint path (e.g., '/chat/completions' or '/v1/chat/completions').\n */\n protected function determineApiEndpoint(): string\n {\n $baseUri = rtrim($this->options['base_uri'] ?? 'https://api.deepseek.com/v1', '/');\n // Check if the effective baseUri already includes the version path\n if (str_ends_with($baseUri, '/v1')) {\n // Already configured with /v1, use relative path\n return '/chat/completions';\n } else {\n // Base URI is likely just the domain (e.g., https://api.deepseek.com),\n // assume the standard v1 path needs to be appended.\n return '/v1/chat/completions';\n }\n }\n\n /**\n * Sends a chat request to the DeepSeek API.\n *\n * @param ChatRequest $request The DTO containing the prompt, history, and options.\n * @return ChatResponse The DTO containing the API response.\n *\n * @throws AuthenticationException If the API key is invalid (401).\n * @throws InvalidResponseException If the API response structure is invalid.\n * @throws LlmApiException For other API errors (rate limits, server errors, etc.).\n */\n public function chat(ChatRequest $request): ChatResponse\n {\n $payload = $this->buildPayload($request);\n\n try {\n $response = $this->httpClient->post($this->apiEndpoint, $payload);\n\n // Handle specific HTTP errors first\n if ($response->status() === 401) {\n $errorMessage = $response->json('error.message', 'DeepSeek Authentication failed - Invalid API Key');\n throw new AuthenticationException($errorMessage, $response->status());\n }\n\n if ($response->failed()) {\n $this->handleErrorResponse($response); // Handles other 4xx/5xx\n }\n\n $responseData = $response->json();\n\n // Validate structure AFTER checking for errors\n if (! $this->isValidResponseStructure($responseData)) {\n // Check if it was an error response that somehow returned 200 OK\n if (isset($responseData['error']['message'])) {\n $errorMessage = $responseData['error']['message'];\n $errorCode = $responseData['error']['code'] ?? 'unknown_error_code';\n throw new InvalidResponseException(\"Invalid response structure, received error details instead: ({$errorCode}) {$errorMessage}\");\n }\n throw new InvalidResponseException('Invalid response structure received from DeepSeek API.');\n }\n\n return $this->mapResponseToDTO($responseData, $request->jsonMode);\n } catch (RequestException $e) {\n // Handles connection errors or other client-side request issues\n throw new LlmApiException(\"HTTP Request Error calling DeepSeek API: {$e->getMessage()}\", $e->getCode(), $e);\n } catch (Throwable $e) {\n // Rethrow our own exceptions, wrap others for clarity\n if ($e instanceof LlmApiException || $e instanceof AuthenticationException || $e instanceof InvalidResponseException) {\n throw $e;\n }\n // Wrap unexpected errors\n throw new LlmApiException(\"An unexpected error occurred during DeepSeek API interaction: {$e->getMessage()}\", $e->getCode(), $e);\n }\n }\n\n /**\n * Builds the payload array for the DeepSeek Chat API request.\n * (Assumes OpenAI compatibility in structure).\n *\n * @param ChatRequest $request The request DTO.\n * @return array The payload ready for JSON encoding.\n */\n protected function buildPayload(ChatRequest $request): array\n {\n $payload = [\n 'model' => $this->model,\n 'messages' => [],\n ];\n\n // Add system message if provided\n if ($request->systemMessage) {\n $payload['messages'][] = ['role' => 'system', 'content' => $request->systemMessage];\n }\n\n // Add history messages\n foreach ($request->history as $message) {\n // DTO validation ensures role/content exist\n $payload['messages'][] = ['role' => $message['role'], 'content' => $message['content']];\n }\n\n // Add the main user prompt\n $payload['messages'][] = ['role' => 'user', 'content' => $request->prompt];\n\n // Add allowed options from the request DTO\n if (! empty($request->options)) {\n // Assuming DeepSeek supports the same options as OpenAI. Verify with DeepSeek docs if needed.\n $allowedOptions = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'stop', 'seed', 'stream'];\n $payload += array_intersect_key($request->options, array_flip($allowedOptions));\n }\n\n // Handle JSON mode parameter\n if ($request->jsonMode) {\n // Assumes DeepSeek supports OpenAI-style JSON mode\n $payload['response_format'] = ['type' => 'json_object'];\n // Note: The user MUST ensure the prompt instructs the model to output JSON.\n }\n\n return $payload;\n }\n\n /**\n * Maps the successful DeepSeek API response data array to the ChatResponse DTO.\n * (Assumes OpenAI compatibility in structure).\n *\n * @param array $responseData The decoded JSON response data from the API.\n * @param bool $wasJsonModeRequested Indicates if the original request asked for JSON.\n * @return ChatResponse The populated response DTO.\n */\n protected function mapResponseToDTO(array $responseData, bool $wasJsonModeRequested): ChatResponse\n {\n // Assuming DeepSeek response structure matches OpenAI\n $content = $responseData['choices'][0]['message']['content'] ?? '';\n $decodedJson = null;\n\n if ($wasJsonModeRequested && ! empty($content)) {\n $decoded = json_decode($content, true);\n if (json_last_error() === JSON_ERROR_NONE) {\n $decodedJson = $decoded;\n }\n }\n\n // Passare un array associativo al costruttore di SimpleDTO\n return new ChatResponse([\n 'content' => $content,\n 'finishReason' => $responseData['choices'][0]['finish_reason'] ?? 'unknown',\n 'model' => $responseData['model'] ?? $this->model,\n 'id' => $responseData['id'] ?? 'unknown',\n 'usage' => $responseData['usage'] ?? null,\n 'isJson' => $wasJsonModeRequested && ($decodedJson !== null),\n 'decodedJsonContent' => $decodedJson,\n 'rawResponse' => $responseData,\n ]);\n }\n\n /**\n * Handles non-successful (non-401) HTTP responses from DeepSeek.\n *\n * @param Response $response The failed HTTP response.\n *\n * @throws LlmApiException Mapped API error.\n */\n protected function handleErrorResponse(Response $response): void\n {\n $statusCode = $response->status();\n $errorData = $response->json('error'); // Attempt to get structured error\n\n $errorMessage = 'Unknown DeepSeek API Error';\n $errorCode = $statusCode; // Default to HTTP status code\n\n if (is_array($errorData)) {\n $errorMessage = $errorData['message'] ?? $response->body();\n $errorCode = $errorData['code'] ?? $statusCode;\n }\n\n // Assuming DeepSeek uses similar error codes/structure to OpenAI\n match ($statusCode) {\n // 401 is handled in the main chat method\n 429 => throw new LlmApiException(\"DeepSeek API Error - Rate Limit Exceeded ({$errorCode}): {$errorMessage}\", $statusCode),\n 400 => throw new LlmApiException(\"DeepSeek API Error - Bad Request ({$errorCode}): {$errorMessage}\", $statusCode),\n 500 => throw new LlmApiException(\"DeepSeek API Error - Internal Server Error ({$errorCode}): {$errorMessage}\", $statusCode),\n default => throw new LlmApiException(\"DeepSeek API Error ({$errorCode}, status:{$statusCode}): {$errorMessage}\", $statusCode),\n };\n }\n\n /**\n * Validates the basic structure of a successful DeepSeek response array.\n * (Assumes OpenAI compatibility).\n *\n * @param array|null $responseData The decoded JSON data from the response.\n * @return bool True if the structure seems valid for a successful response, false otherwise.\n */\n protected function isValidResponseStructure(?array $responseData): bool\n {\n if ($responseData === null) {\n return false;\n }\n\n // Assuming same structure as OpenAI\n return isset($responseData['id'], $responseData['model'], $responseData['choices']) &&\n is_array($responseData['choices']) &&\n count($responseData['choices']) > 0 &&\n isset($responseData['choices'][0]['message'], $responseData['choices'][0]['message']['content']); // Check nested content\n }\n}\n"}, "727657d490f46215f8d7f07cec494a7412d7da5b": {"t": 1785760170.5891604, "n": 11414, "h": "83ad961ff03fb47f", "c": "httpClient = $this->configureHttpClient();\n // Determine the correct endpoint based on the base URI provided\n $this->apiEndpoint = $this->determineApiEndpoint();\n }\n\n /**\n * Configures the HTTP client with base URI and authentication.\n *\n * @return PendingRequest The configured HTTP client.\n */\n protected function configureHttpClient(): PendingRequest\n {\n // Default base URI for DeepSeek's OpenAI-compatible endpoint\n $baseUri = $this->options['base_uri'] ?? 'https://api.deepseek.com/v1';\n $timeout = $this->options['timeout'] ?? 30;\n\n return $this->httpFactory->baseUrl($baseUri)\n ->withToken($this->apiKey)\n ->acceptJson()\n ->asJson()\n ->timeout($timeout);\n }\n\n /**\n * Determines the correct relative API endpoint path based on the configured base URI.\n *\n * Handles cases where the base URI might or might not include the '/v1' path segment.\n *\n * @return string The API endpoint path (e.g., '/chat/completions' or '/v1/chat/completions').\n */\n protected function determineApiEndpoint(): string\n {\n $baseUri = rtrim($this->options['base_uri'] ?? 'https://api.deepseek.com/v1', '/');\n // Check if the effective baseUri already includes the version path\n if (str_ends_with($baseUri, '/v1')) {\n // Already configured with /v1, use relative path\n return '/chat/completions';\n } else {\n // Base URI is likely just the domain (e.g., https://api.deepseek.com),\n // assume the standard v1 path needs to be appended.\n return '/v1/chat/completions';\n }\n }\n\n /**\n * Sends a chat request to the DeepSeek API.\n *\n * @param ChatRequest $request The DTO containing the prompt, history, and options.\n * @return ChatResponse The DTO containing the API response.\n *\n * @throws AuthenticationException If the API key is invalid (401).\n * @throws InvalidResponseException If the API response structure is invalid.\n * @throws LlmApiException For other API errors (rate limits, server errors, etc.).\n */\n public function chat(ChatRequest $request): ChatResponse\n {\n $payload = $this->buildPayload($request);\n\n try {\n $response = $this->httpClient->post($this->apiEndpoint, $payload);\n\n // Handle specific HTTP errors first\n if ($response->status() === 401) {\n $errorMessage = $response->json('error.message', 'DeepSeek Authentication failed - Invalid API Key');\n throw new AuthenticationException($errorMessage, $response->status());\n }\n\n if ($response->failed()) {\n $this->handleErrorResponse($response); // Handles other 4xx/5xx\n }\n\n $responseData = $response->json();\n\n // Validate structure AFTER checking for errors\n if (! $this->isValidResponseStructure($responseData)) {\n // Check if it was an error response that somehow returned 200 OK\n if (isset($responseData['error']['message'])) {\n $errorMessage = $responseData['error']['message'];\n $errorCode = $responseData['error']['code'] ?? 'unknown_error_code';\n throw new InvalidResponseException(\"Invalid response structure, received error details instead: ({$errorCode}) {$errorMessage}\");\n }\n throw new InvalidResponseException('Invalid response structure received from DeepSeek API.');\n }\n\n return $this->mapResponseToDTO($responseData, $request->jsonMode);\n } catch (RequestException $e) {\n // Handles connection errors or other client-side request issues\n throw new LlmApiException(\"HTTP Request Error calling DeepSeek API: {$e->getMessage()}\", $e->getCode(), $e);\n } catch (Throwable $e) {\n // Rethrow our own exceptions, wrap others for clarity\n if ($e instanceof LlmApiException || $e instanceof AuthenticationException || $e instanceof InvalidResponseException) {\n throw $e;\n }\n // Wrap unexpected errors\n throw new LlmApiException(\"An unexpected error occurred during DeepSeek API interaction: {$e->getMessage()}\", $e->getCode(), $e);\n }\n }\n\n /**\n * Builds the payload array for the DeepSeek Chat API request.\n * (Assumes OpenAI compatibility in structure).\n *\n * @param ChatRequest $request The request DTO.\n * @return array The payload ready for JSON encoding.\n */\n protected function buildPayload(ChatRequest $request): array\n {\n $payload = [\n 'model' => $this->model,\n 'messages' => [],\n ];\n\n // Add system message if provided\n if ($request->systemMessage) {\n $payload['messages'][] = ['role' => 'system', 'content' => $request->systemMessage];\n }\n\n // Add history messages\n foreach ($request->history as $message) {\n // DTO validation ensures role/content exist\n $payload['messages'][] = ['role' => $message['role'], 'content' => $message['content']];\n }\n\n // Add the main user prompt\n $payload['messages'][] = ['role' => 'user', 'content' => $request->prompt];\n\n // Add allowed options from the request DTO\n if (! empty($request->options)) {\n // Assuming DeepSeek supports the same options as OpenAI. Verify with DeepSeek docs if needed.\n $allowedOptions = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'stop', 'seed', 'stream'];\n $payload += array_intersect_key($request->options, array_flip($allowedOptions));\n }\n\n // Handle JSON mode parameter\n if ($request->jsonMode) {\n // Assumes DeepSeek supports OpenAI-style JSON mode\n $payload['response_format'] = ['type' => 'json_object'];\n // Note: The user MUST ensure the prompt instructs the model to output JSON.\n }\n\n return $payload;\n }\n\n /**\n * Maps the successful DeepSeek API response data array to the ChatResponse DTO.\n * (Assumes OpenAI compatibility in structure).\n *\n * @param array $responseData The decoded JSON response data from the API.\n * @param bool $wasJsonModeRequested Indicates if the original request asked for JSON.\n * @return ChatResponse The populated response DTO.\n */\n protected function mapResponseToDTO(array $responseData, bool $wasJsonModeRequested): ChatResponse\n {\n // Assuming DeepSeek response structure matches OpenAI\n $content = $responseData['choices'][0]['message']['content'] ?? '';\n $decodedJson = null;\n\n if ($wasJsonModeRequested && ! empty($content)) {\n $decoded = json_decode($content, true);\n if (json_last_error() === JSON_ERROR_NONE) {\n $decodedJson = $decoded;\n }\n }\n\n // Passare un array associativo al costruttore di SimpleDTO\n return new ChatResponse([\n 'content' => $content,\n 'finishReason' => $responseData['choices'][0]['finish_reason'] ?? 'unknown',\n 'model' => $responseData['model'] ?? $this->model,\n 'id' => $responseData['id'] ?? 'unknown',\n 'usage' => $responseData['usage'] ?? null,\n 'isJson' => $wasJsonModeRequested && ($decodedJson !== null),\n 'decodedJsonContent' => $decodedJson,\n 'rawResponse' => $responseData,\n ]);\n }\n\n /**\n * Handles non-successful (non-401) HTTP responses from DeepSeek.\n *\n * @param Response $response The failed HTTP response.\n *\n * @throws LlmApiException Mapped API error.\n */\n protected function handleErrorResponse(Response $response): void\n {\n $statusCode = $response->status();\n $errorData = $response->json('error'); // Attempt to get structured error\n\n $errorMessage = 'Unknown DeepSeek API Error';\n $errorCode = $statusCode; // Default to HTTP status code\n\n if (is_array($errorData)) {\n $errorMessage = $errorData['message'] ?? $response->body();\n $errorCode = $errorData['code'] ?? $statusCode;\n }\n\n // Assuming DeepSeek uses similar error codes/structure to OpenAI\n match ($statusCode) {\n // 401 is handled in the main chat method\n 429 => throw new LlmApiException(\"DeepSeek API Error - Rate Limit Exceeded ({$errorCode}): {$errorMessage}\", $statusCode),\n 400 => throw new LlmApiException(\"DeepSeek API Error - Bad Request ({$errorCode}): {$errorMessage}\", $statusCode),\n 500 => throw new LlmApiException(\"DeepSeek API Error - Internal Server Error ({$errorCode}): {$errorMessage}\", $statusCode),\n default => throw new LlmApiException(\"DeepSeek API Error ({$errorCode}, status:{$statusCode}): {$errorMessage}\", $statusCode),\n };\n }\n\n /**\n * Validates the basic structure of a successful DeepSeek response array.\n * (Assumes OpenAI compatibility).\n *\n * @param array|null $responseData The decoded JSON data from the response.\n * @return bool True if the structure seems valid for a successful response, false otherwise.\n */\n protected function isValidResponseStructure(?array $responseData): bool\n {\n if ($responseData === null) {\n return false;\n }\n\n // Assuming same structure as OpenAI\n return isset($responseData['id'], $responseData['model'], $responseData['choices']) &&\n is_array($responseData['choices']) &&\n count($responseData['choices']) > 0 &&\n isset($responseData['choices'][0]['message'], $responseData['choices'][0]['message']['content']); // Check nested content\n }\n}\n"}, "36548885bc90321499aced5cc7d63be8564d298e": {"t": 1785760193.6933906, "n": 11518, "h": "f7424fa6904c419c", "c": "httpClient = $this->configureHttpClient();\n // Determine the correct endpoint based on the base URI provided\n $this->apiEndpoint = $this->determineApiEndpoint();\n }\n\n /**\n * Configures the HTTP client with base URI and authentication.\n *\n * @return PendingRequest The configured HTTP client.\n */\n protected function configureHttpClient(): PendingRequest\n {\n // Default base URI for DeepSeek's OpenAI-compatible endpoint\n $baseUri = $this->options['base_uri'] ?? 'https://api.deepseek.com/v1';\n $timeout = $this->options['timeout'] ?? 30;\n\n return $this->httpFactory->baseUrl($baseUri)\n ->withToken($this->apiKey)\n ->acceptJson()\n ->contentTypeJson() // Ensure Content-Type is set\n ->timeout($timeout);\n }\n\n /**\n * Determines the correct relative API endpoint path based on the configured base URI.\n *\n * Handles cases where the base URI might or might not include the '/v1' path segment.\n *\n * @return string The API endpoint path (e.g., '/chat/completions' or '/v1/chat/completions').\n */\n protected function determineApiEndpoint(): string\n {\n $baseUri = rtrim($this->options['base_uri'] ?? 'https://api.deepseek.com/v1', '/');\n // Check if the effective baseUri already includes the version path\n if (str_ends_with($baseUri, '/v1')) {\n // Already configured with /v1, use relative path\n return '/chat/completions';\n } else {\n // Base URI is likely just the domain (e.g., https://api.deepseek.com),\n // assume the standard v1 path needs to be appended.\n return '/v1/chat/completions';\n }\n }\n\n\n /**\n * Sends a chat request to the DeepSeek API.\n *\n * @param ChatRequest $request The DTO containing the prompt, history, and options.\n * @return ChatResponse The DTO containing the API response.\n * @throws AuthenticationException If the API key is invalid (401).\n * @throws InvalidResponseException If the API response structure is invalid.\n * @throws LlmApiException For other API errors (rate limits, server errors, etc.).\n */\n public function chat(ChatRequest $request): ChatResponse\n {\n $payload = $this->buildPayload($request);\n\n try {\n $response = $this->httpClient->post($this->apiEndpoint, $payload);\n\n // Handle specific HTTP errors first\n if ($response->status() === 401) {\n $errorMessage = $response->json('error.message', 'DeepSeek Authentication failed - Invalid API Key');\n throw new AuthenticationException($errorMessage, $response->status());\n }\n\n if ($response->failed()) {\n $this->handleErrorResponse($response); // Handles other 4xx/5xx\n }\n\n $responseData = $response->json();\n\n // Validate structure AFTER checking for errors\n if (! $this->isValidResponseStructure($responseData)) {\n // Check if it was an error response that somehow returned 200 OK\n if (isset($responseData['error']['message'])) {\n $errorMessage = $responseData['error']['message'];\n $errorCode = $responseData['error']['code'] ?? 'unknown_error_code';\n throw new InvalidResponseException(\"Invalid response structure, received error details instead: ({$errorCode}) {$errorMessage}\");\n }\n throw new InvalidResponseException('Invalid response structure received from DeepSeek API.');\n }\n\n return $this->mapResponseToDTO($responseData, $request->jsonMode);\n } catch (RequestException $e) {\n // Handles connection errors or other client-side request issues\n throw new LlmApiException(\"HTTP Request Error calling DeepSeek API: {$e->getMessage()}\", $e->getCode(), $e);\n } catch (Throwable $e) {\n // Rethrow our own exceptions, wrap others for clarity\n if ($e instanceof LlmApiException || $e instanceof AuthenticationException || $e instanceof InvalidResponseException) {\n throw $e;\n }\n // Wrap unexpected errors\n throw new LlmApiException(\"An unexpected error occurred during DeepSeek API interaction: {$e->getMessage()}\", $e->getCode(), $e);\n }\n }\n\n /**\n * Builds the payload array for the DeepSeek Chat API request.\n * (Assumes OpenAI compatibility in structure).\n *\n * @param ChatRequest $request The request DTO.\n * @return array The payload ready for JSON encoding.\n */\n protected function buildPayload(ChatRequest $request): array\n {\n $payload = [\n 'model' => $this->model,\n 'messages' => [],\n ];\n\n // Add system message if provided\n if ($request->systemMessage) {\n $payload['messages'][] = ['role' => 'system', 'content' => $request->systemMessage];\n }\n\n // Add history messages\n foreach ($request->history as $message) {\n // DTO validation ensures role/content exist\n $payload['messages'][] = ['role' => $message['role'], 'content' => $message['content']];\n }\n\n // Add the main user prompt\n $payload['messages'][] = ['role' => 'user', 'content' => $request->prompt];\n\n // Add allowed options from the request DTO\n if (!empty($request->options)) {\n // Assuming DeepSeek supports the same options as OpenAI. Verify with DeepSeek docs if needed.\n $allowedOptions = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'stop', 'seed', 'stream'];\n $payload += array_intersect_key($request->options, array_flip($allowedOptions));\n }\n\n // Handle JSON mode parameter\n if ($request->jsonMode) {\n // Assumes DeepSeek supports OpenAI-style JSON mode\n $payload['response_format'] = ['type' => 'json_object'];\n // Note: The user MUST ensure the prompt instructs the model to output JSON.\n }\n\n return $payload;\n }\n\n /**\n * Maps the successful DeepSeek API response data array to the ChatResponse DTO.\n * (Assumes OpenAI compatibility in structure).\n *\n * @param array $responseData The decoded JSON response data from the API.\n * @param bool $wasJsonModeRequested Indicates if the original request asked for JSON.\n * @return ChatResponse The populated response DTO.\n */\n protected function mapResponseToDTO(array $responseData, bool $wasJsonModeRequested): ChatResponse\n {\n // Assuming DeepSeek response structure matches OpenAI\n $content = $responseData['choices'][0]['message']['content'] ?? '';\n $decodedJson = null;\n\n // Attempt to decode content if JSON mode was requested\n if ($wasJsonModeRequested && !empty($content)) {\n $decoded = json_decode($content, true);\n if (json_last_error() === JSON_ERROR_NONE) {\n $decodedJson = $decoded;\n }\n // If decoding fails, $decodedJson remains null, $content holds the raw string.\n }\n\n return new ChatResponse(\n $content, // Raw content string (might be JSON)\n $responseData['choices'][0]['finish_reason'] ?? 'unknown',\n $responseData['model'] ?? $this->model,\n $responseData['id'] ?? 'unknown',\n $responseData['usage'] ?? null,\n $wasJsonModeRequested && ($decodedJson !== null), // isJson true only if requested AND decoded\n $decodedJson, // Decoded array/value or null\n $responseData // Original raw response\n );\n }\n\n /**\n * Handles non-successful (non-401) HTTP responses from DeepSeek.\n *\n * @param Response $response The failed HTTP response.\n * @throws LlmApiException Mapped API error.\n */\n protected function handleErrorResponse(Response $response): void\n {\n $statusCode = $response->status();\n $errorData = $response->json('error'); // Attempt to get structured error\n\n $errorMessage = 'Unknown DeepSeek API Error';\n $errorCode = $statusCode; // Default to HTTP status code\n\n if (is_array($errorData)) {\n $errorMessage = $errorData['message'] ?? $response->body();\n $errorCode = $errorData['code'] ?? $statusCode;\n }\n\n // Assuming DeepSeek uses similar error codes/structure to OpenAI\n match ($statusCode) {\n // 401 is handled in the main chat method\n 429 => throw new LlmApiException(\"DeepSeek API Error - Rate Limit Exceeded ({$errorCode}): {$errorMessage}\", $statusCode),\n 400 => throw new LlmApiException(\"DeepSeek API Error - Bad Request ({$errorCode}): {$errorMessage}\", $statusCode),\n 500 => throw new LlmApiException(\"DeepSeek API Error - Internal Server Error ({$errorCode}): {$errorMessage}\", $statusCode),\n default => throw new LlmApiException(\"DeepSeek API Error ({$errorCode}, status:{$statusCode}): {$errorMessage}\", $statusCode),\n };\n }\n\n /**\n * Validates the basic structure of a successful DeepSeek response array.\n * (Assumes OpenAI compatibility).\n *\n * @param array|null $responseData The decoded JSON data from the response.\n * @return bool True if the structure seems valid for a successful response, false otherwise.\n */\n protected function isValidResponseStructure(?array $responseData): bool\n {\n if ($responseData === null) {\n return false;\n }\n\n // Assuming same structure as OpenAI\n return isset($responseData['id'], $responseData['model'], $responseData['choices']) &&\n is_array($responseData['choices']) &&\n count($responseData['choices']) > 0 &&\n isset($responseData['choices'][0]['message'], $responseData['choices'][0]['message']['content']); // Check nested content\n }\n}\n"}, "4d19d1b13b2c37dc2ae5ef095aa4f6e00f9167eb": {"t": 1785760194.9064472, "n": 8045, "h": "c32b65941285e947", "c": "httpClient = $this->configureHttpClient();\n // Determine the correct endpoint based on the base URI provided\n $this->apiEndpoint = $this->determineApiEndpoint();\n }\n\n /**\n * Configures the HTTP client with base URI and headers (similar to OpenAI).\n */\n protected function configureHttpClient(): PendingRequest\n {\n // Default base URI for DeepSeek's OpenAI-compatible endpoint\n $baseUri = $this->options['base_uri'] ?? 'https://api.deepseek.com/v1';\n $timeout = $this->options['timeout'] ?? 30;\n\n return $this->httpFactory->baseUrl($baseUri)\n ->withToken($this->apiKey)\n ->acceptJson()\n ->contentTypeJson() // Ensure Content-Type is set\n ->timeout($timeout);\n }\n\n /**\n * Determines the correct relative API endpoint path.\n * If the base_uri already ends with /v1, use a relative path.\n * Otherwise, use the full path.\n */\n protected function determineApiEndpoint(): string\n {\n $baseUri = rtrim($this->options['base_uri'] ?? 'https://api.deepseek.com/v1', '/');\n // Check if the effective baseUri already includes the version path\n if (str_ends_with($baseUri, '/v1')) {\n // Already configured with /v1, use relative path\n return '/chat/completions';\n } else {\n // Base URI is likely just the domain, use full path (assuming v1)\n // Or adjust if DeepSeek offers non-v1 endpoints via this client later\n return '/v1/chat/completions';\n }\n }\n\n\n /**\n * Sends a chat request to the DeepSeek API.\n *\n * @throws AuthenticationException\n * @throws InvalidResponseException\n * @throws LlmApiException\n */\n public function chat(ChatRequest $request): ChatResponse\n {\n $payload = $this->buildPayload($request);\n\n try {\n $response = $this->httpClient->post($this->apiEndpoint, $payload);\n\n // Handle specific HTTP errors first before checking success\n if ($response->status() === 401) {\n throw new AuthenticationException(\n $response->json('error.message', 'DeepSeek Authentication failed'),\n $response->status()\n );\n }\n\n if ($response->failed()) {\n $this->handleErrorResponse($response); // Handles other 4xx/5xx\n }\n\n $responseData = $response->json();\n\n // Validate structure AFTER checking for errors\n if (! $this->isValidResponseStructure($responseData)) {\n throw new InvalidResponseException('Invalid response structure received from DeepSeek API.');\n }\n\n return $this->mapResponseToDTO($responseData, $request->jsonMode);\n } catch (RequestException $e) {\n throw new LlmApiException(\"HTTP Request Error calling DeepSeek API: {$e->getMessage()}\", $e->getCode(), $e);\n } catch (Throwable $e) {\n // Rethrow our own exceptions, wrap others\n if ($e instanceof LlmApiException || $e instanceof AuthenticationException || $e instanceof InvalidResponseException) {\n throw $e;\n }\n throw new LlmApiException(\"An unexpected error occurred calling DeepSeek API: {$e->getMessage()}\", $e->getCode(), $e);\n }\n }\n\n /**\n * Builds the payload for the DeepSeek API request (similar to OpenAI).\n */\n protected function buildPayload(ChatRequest $request): array\n {\n $payload = [\n 'model' => $this->model,\n 'messages' => [],\n ];\n\n if ($request->systemMessage) {\n $payload['messages'][] = ['role' => 'system', 'content' => $request->systemMessage];\n }\n\n foreach ($request->history as $message) {\n if (isset($message['role'], $message['content'])) {\n $payload['messages'][] = ['role' => $message['role'], 'content' => $message['content']];\n }\n }\n\n $payload['messages'][] = ['role' => 'user', 'content' => $request->prompt];\n\n if (!empty($request->options)) {\n // Assuming DeepSeek uses the same options as OpenAI\n $allowedOptions = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'stop', 'seed', 'stream']; // Added stream\n $payload += array_intersect_key($request->options, array_flip($allowedOptions));\n }\n\n if ($request->jsonMode) {\n $payload['response_format'] = ['type' => 'json_object'];\n }\n\n return $payload;\n }\n\n /**\n * Maps the successful DeepSeek API response to the ChatResponse DTO (similar to OpenAI).\n */\n protected function mapResponseToDTO(array $responseData, bool $wasJsonModeRequested): ChatResponse\n {\n // Assuming DeepSeek response structure matches OpenAI\n $content = $responseData['choices'][0]['message']['content'] ?? '';\n $decodedJson = null;\n\n if ($wasJsonModeRequested) {\n $decoded = json_decode($content, true);\n if (json_last_error() === JSON_ERROR_NONE) {\n $decodedJson = $decoded;\n }\n }\n\n return new ChatResponse(\n $content,\n $responseData['choices'][0]['finish_reason'] ?? 'unknown',\n $responseData['model'] ?? $this->model,\n $responseData['id'] ?? 'unknown',\n $responseData['usage'] ?? null,\n $wasJsonModeRequested,\n $decodedJson,\n $responseData\n );\n }\n\n /**\n * Handles non-successful HTTP responses.\n *\n * @throws LlmApiException\n */\n protected function handleErrorResponse(Response $response): void\n {\n $statusCode = $response->status();\n $errorData = $response->json('error');\n $errorMessage = is_array($errorData) && isset($errorData['message']) ? $errorData['message'] : $response->body();\n $errorCode = is_array($errorData) && isset($errorData['code']) ? $errorData['code'] : $statusCode;\n\n // Assuming DeepSeek uses similar error codes/structure to OpenAI\n match ($statusCode) {\n // 401 is handled in the main chat method\n 429 => throw new LlmApiException(\"DeepSeek API Error - Rate Limit Exceeded ({$errorCode}): {$errorMessage}\", $statusCode), // Consider RateLimitException\n default => throw new LlmApiException(\"DeepSeek API Error ({$errorCode}, status:{$statusCode}): {$errorMessage}\", $statusCode),\n };\n }\n\n /**\n * Validates the basic structure of the DeepSeek response (assuming OpenAI compatibility).\n */\n protected function isValidResponseStructure(?array $responseData): bool\n {\n if ($responseData === null) {\n return false;\n }\n\n // Assuming same structure as OpenAI\n return isset($responseData['id'], $responseData['model'], $responseData['choices']) &&\n is_array($responseData['choices']) &&\n count($responseData['choices']) > 0 &&\n isset($responseData['choices'][0]['message']['content']);\n }\n}\n"}, "988a78cacfb53dc028e943f9be35c20d0ae42a75": {"t": 1785760196.2301543, "n": 5670, "h": "c1d16cb41613f74e", "c": "httpClient = $this->configureHttpClient();\n }\n\n /**\n * Configures the HTTP client with base URI and headers (similar to OpenAI).\n */\n protected function configureHttpClient(): PendingRequest\n {\n $baseUri = $this->options['base_uri'] ?? 'https://api.deepseek.com'; // Or /v1 for OpenAI compatibility endpoint\n $timeout = $this->options['timeout'] ?? 30;\n\n return $this->httpFactory->baseUrl($baseUri)\n ->withToken($this->apiKey)\n ->acceptJson()\n ->timeout($timeout);\n }\n\n /**\n * Sends a chat request to the DeepSeek API.\n *\n * @param ChatRequest $request The DTO containing the request data.\n * @return ChatResponse The DTO containing the model's response.\n * @throws \\Bramato\\LaravelAi\\Exceptions\\LlmApiException On API errors.\n * @throws RequestException\n */\n public function chat(ChatRequest $request): ChatResponse\n {\n // TODO: Implement DeepSeek API call logic (expected to be very similar to OpenAI)\n // 1. Prepare request payload\n // 2. Make POST request to /chat/completions endpoint\n // 3. Handle potential RequestException\n // 4. Parse the response\n // 5. Handle API errors\n // 6. Map successful response to ChatResponse DTO\n\n $payload = $this->buildPayload($request);\n\n try {\n // Use /v1/chat/completions if using the OpenAI compatibility endpoint base URI\n $endpoint = ($this->options['base_uri'] ?? '') === 'https://api.deepseek.com/v1'\n ? '/chat/completions'\n : '/chat/completions'; // Default endpoint if using base api.deepseek.com\n\n // Workaround: Check if base_uri ends with v1 and adjust endpoint. Cleaner way? Maybe store full endpoint path in config?\n if (str_ends_with($this->options['base_uri'] ?? '', '/v1')) {\n $endpoint = '/chat/completions'; // Use relative if base URI already includes /v1\n } else {\n $endpoint = '/chat/completions'; // Use full path if base URI is just the domain\n }\n\n\n $response = $this->httpClient->post($endpoint, $payload);\n\n if (! $response->successful()) {\n // TODO: Throw custom exception\n $response->throw();\n }\n\n return $this->mapResponseToDTO($response->json() ?? [], $request->jsonMode);\n } catch (RequestException $e) {\n // TODO: Wrap the exception\n throw $e;\n }\n }\n\n /**\n * Builds the payload for the DeepSeek API request (similar to OpenAI).\n */\n protected function buildPayload(ChatRequest $request): array\n {\n // Expected to be identical or very similar to OpenAIClient::buildPayload\n $payload = [\n 'model' => $this->model,\n 'messages' => [],\n ];\n\n if ($request->systemMessage) {\n $payload['messages'][] = ['role' => 'system', 'content' => $request->systemMessage];\n }\n\n foreach ($request->history as $message) {\n if (isset($message['role'], $message['content'])) {\n $payload['messages'][] = ['role' => $message['role'], 'content' => $message['content']];\n }\n }\n\n $payload['messages'][] = ['role' => 'user', 'content' => $request->prompt];\n\n if (!empty($request->options)) {\n // Check DeepSeek docs for specific supported options\n $allowedOptions = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'stop', 'seed'];\n $payload += array_intersect_key($request->options, array_flip($allowedOptions));\n }\n\n if ($request->jsonMode) {\n $payload['response_format'] = ['type' => 'json_object'];\n }\n\n return $payload;\n }\n\n /**\n * Maps the successful DeepSeek API response to the ChatResponse DTO (similar to OpenAI).\n */\n protected function mapResponseToDTO(array $responseData, bool $wasJsonModeRequested): ChatResponse\n {\n // Expected to be identical or very similar to OpenAIClient::mapResponseToDTO\n $content = $responseData['choices'][0]['message']['content'] ?? '';\n $decodedJson = null;\n\n if ($wasJsonModeRequested) {\n $decoded = json_decode($content, true);\n if (json_last_error() === JSON_ERROR_NONE) {\n $decodedJson = $decoded;\n }\n }\n\n return new ChatResponse(\n content: $content,\n finishReason: $responseData['choices'][0]['finish_reason'] ?? 'unknown',\n model: $responseData['model'] ?? $this->model,\n id: $responseData['id'] ?? 'unknown',\n usage: $responseData['usage'] ?? null,\n isJson: $wasJsonModeRequested,\n decodedJsonContent: $decodedJson,\n rawResponse: $responseData\n );\n }\n}\n"}}, "tests/Feature/DeepSeekClientTest.php": {"fb44f12874a25c1a212c7b84771b5047eef1c77f": {"t": 1786068385.7592878, "n": 5075, "h": "21fb857b05d2eb1e", "c": "set('laravel-ai.default', 'deepseek');\n config()->set('laravel-ai.providers.deepseek.api_key', 'test-deepseek-key');\n config()->set('laravel-ai.providers.deepseek.model', 'deepseek-test');\n // Test with the default base_uri (ends in /v1)\n config()->set('laravel-ai.providers.deepseek.options', []); // Reset options\n});\n\nit('can get successful response via interface', function () {\n // Uses global helper: getFakeSuccessResponseData()\n $fakeResponseData = getFakeSuccessResponseData('ds-123', 'deepseek-test', 'DeepSeek says hi');\n Http::fake([\n 'api.deepseek.com/v1/chat/completions' => Http::response($fakeResponseData, 200),\n ]);\n\n $client = app(LlmClientInterface::class);\n $request = new ChatRequest(['prompt' => 'Hello DeepSeek']);\n $response = $client->chat($request);\n\n expect($response)->toBeInstanceOf(ChatResponse::class)\n ->and($response->content)->toBe('DeepSeek says hi')\n ->and($response->id)->toBe('ds-123')\n ->and($response->model)->toBe('deepseek-test')\n ->and($response->rawResponse)->toBe($fakeResponseData);\n\n Http::assertSentCount(1);\n Http::assertSent(function ($httpRequest) {\n return $httpRequest->url() === 'https://api.deepseek.com/v1/chat/completions' && // Check the full URL\n $httpRequest->method() === 'POST' &&\n $httpRequest->hasHeader('Authorization', 'Bearer test-deepseek-key') &&\n isset($httpRequest->data()['model']) && $httpRequest->data()['model'] === 'deepseek-test';\n });\n});\n\nit('can get successful response with custom base_uri (no /v1)', function () {\n // Override base_uri for this test\n config()->set('laravel-ai.providers.deepseek.options.base_uri', 'https://custom.deepseek.endpoint');\n\n // Uses global helper: getFakeSuccessResponseData()\n $fakeResponseData = getFakeSuccessResponseData('ds-456', 'deepseek-test', 'DeepSeek custom endpoint');\n Http::fake([\n 'custom.deepseek.endpoint/v1/chat/completions' => Http::response($fakeResponseData, 200), // Expect client to add /v1\n ]);\n\n $client = app(LlmClientInterface::class); // Re-resolve client after changing config\n $request = new ChatRequest(['prompt' => 'Hello Custom DeepSeek']);\n $response = $client->chat($request);\n\n expect($response)->toBeInstanceOf(ChatResponse::class)\n ->and($response->content)->toBe('DeepSeek custom endpoint')\n ->and($response->id)->toBe('ds-456');\n\n Http::assertSentCount(1);\n Http::assertSent(function ($httpRequest) {\n // Client should construct the correct URL\n return $httpRequest->url() === 'https://custom.deepseek.endpoint/v1/chat/completions' &&\n $httpRequest->hasHeader('Authorization', 'Bearer test-deepseek-key');\n });\n});\n\nit('throws AuthenticationException on 401 error', function () {\n // Uses global helper: getFakeErrorResponseData()\n $errorResponse = getFakeErrorResponseData('Invalid API key', 'auth_error', 'invalid_key');\n Http::fake([\n 'api.deepseek.com/v1/chat/completions' => Http::response($errorResponse, 401),\n ]);\n\n $client = app(LlmClientInterface::class);\n $request = new ChatRequest(['prompt' => 'Test DeepSeek Auth']);\n\n // Expect the specific message from DeepSeekClient handler\n expect(fn () => $client->chat($request))->toThrow(AuthenticationException::class, 'Invalid API key');\n});\n\nit('throws LlmApiException on 500 error', function () {\n // Uses global helper: getFakeErrorResponseData()\n $errorResponse = getFakeErrorResponseData('Server busy', 'server_error', 'busy');\n Http::fake([\n 'api.deepseek.com/v1/chat/completions' => Http::response($errorResponse, 500),\n ]);\n\n $client = app(LlmClientInterface::class);\n $request = new ChatRequest(['prompt' => 'Test DeepSeek Server Error']);\n\n expect(fn () => $client->chat($request))->toThrow(LlmApiException::class, 'DeepSeek API Error - Internal Server Error (busy): Server busy');\n});\n\nit('throws InvalidResponseException on malformed success response', function () {\n $malformedResponseData = [\n 'id' => 'ds-malformed',\n // 'choices' missing\n ];\n Http::fake([\n 'api.deepseek.com/v1/chat/completions' => Http::response($malformedResponseData, 200),\n ]);\n\n $client = app(LlmClientInterface::class);\n $request = new ChatRequest(['prompt' => 'Test DeepSeek Malformed']);\n\n expect(fn () => $client->chat($request))->toThrow(InvalidResponseException::class, 'Invalid response structure received from DeepSeek API.');\n});\n"}}}}