1+ #ifndef PERPLEXITY_CPP_CLIENT_H
2+ #define PERPLEXITY_CPP_CLIENT_H
3+ #pragma once
4+
5+ #include < future>
6+ #include < functional>
7+ #include < sstream>
8+ #include < nlohmann/json.hpp>
9+ #include " config.h"
10+ #include " models.h"
11+ #include " HttpClient.h"
12+ #include " RateLimiter.h"
13+ #include " exceptions.h"
14+
15+ namespace perplexity {
16+
17+ using json = nlohmann::json;
18+ using StreamCallback = std::function<void (const StreamChunk&)>;
19+
20+ /* *
21+ * @brief The main client of the Perplexity API
22+ *
23+ * Patterns:
24+ * - RAII for resource management
25+ * - Strategy pattern for various types of requests
26+ * - Template Method for processing responses with retry logic
27+ */
28+ class Client {
29+ private:
30+ Config config_;
31+ RateLimiter rate_limiter_;
32+ CurlGlobalInit curl_init_;
33+
34+ /* *
35+ * @brief HTTP response processing and error conversion
36+ */
37+ void handle_http_response (long status_code, const std::string& response_body) {
38+ if (status_code >= 200 && status_code < 300 ) {
39+ return ;
40+ }
41+
42+ std::string error_message = " HTTP " + std::to_string (status_code);
43+ try {
44+ auto j = json::parse (response_body);
45+ if (j.contains (" error" )) {
46+ if (j[" error" ].is_string ()) {
47+ error_message = j[" error" ].get <std::string>();
48+ } else if (j[" error" ].is_object () && j[" error" ].contains (" message" )) {
49+ error_message = j[" error" ][" message" ].get <std::string>();
50+ }
51+ }
52+ } catch (...) {
53+ if (!response_body.empty () && response_body.size () < 200 ) {
54+ error_message = response_body;
55+ }
56+ }
57+
58+ switch (status_code) {
59+ case 400 :
60+ throw ValidationError (error_message);
61+ case 401 :
62+ case 403 :
63+ throw AuthenticationError (error_message);
64+ case 429 : {
65+ std::optional<int > retry_after;
66+ try {
67+ auto j = json::parse (response_body);
68+ if (j.contains (" retry_after" )) {
69+ retry_after = j[" retry_after" ].get <int >();
70+ }
71+ } catch (...) {}
72+ throw RateLimitError (error_message, retry_after);
73+ }
74+ case 500 :
75+ case 502 :
76+ case 503 :
77+ case 504 :
78+ throw ServerError (error_message, status_code);
79+ default :
80+ throw NetworkError (error_message, status_code);
81+ }
82+ }
83+
84+ /* *
85+ * @brief Template Method for executing a query with retry logic
86+ */
87+ std::string execute_request_with_retry (
88+ const std::function<std::string(HttpClient&)>& request_func
89+ ) {
90+ int attempt = 0 ;
91+ std::exception_ptr last_exception;
92+
93+ while (attempt <= config_.get_max_retries ()) {
94+ try {
95+ rate_limiter_.wait_if_needed ();
96+
97+ HttpClient http_client (config_);
98+
99+ std::string response = request_func (http_client);
100+ long status_code = http_client.get_response_code ();
101+
102+ handle_http_response (status_code, response);
103+
104+ return response;
105+
106+ } catch (const RateLimitError&) {
107+ throw ;
108+ } catch (const AuthenticationError&) {
109+ throw ;
110+ } catch (const ValidationError&) {
111+ throw ;
112+ } catch (const ServerError& e) {
113+ last_exception = std::current_exception ();
114+
115+ if (attempt < config_.get_max_retries ()) {
116+ // Exponential backoff
117+ auto wait_ms = std::chrono::milliseconds (100 * (1 << attempt));
118+ std::this_thread::sleep_for (wait_ms);
119+ }
120+ } catch (const NetworkError&) {
121+ last_exception = std::current_exception ();
122+
123+ if (attempt < config_.get_max_retries ()) {
124+ auto wait_ms = std::chrono::milliseconds (100 * (1 << attempt));
125+ std::this_thread::sleep_for (wait_ms);
126+ }
127+ }
128+
129+ attempt++;
130+ }
131+
132+ if (last_exception) {
133+ std::rethrow_exception (last_exception);
134+ }
135+
136+ throw NetworkError (" Request failed after " + std::to_string (config_.get_max_retries ()) + " retries" );
137+ }
138+
139+ /* *
140+ * @brief Preparing headers for the request
141+ */
142+ void prepare_headers (HttpClient& http_client) const {
143+ http_client.add_header (" Content-Type: application/json" );
144+ http_client.add_header (" Authorization: Bearer " + config_.get_api_key ());
145+ http_client.add_header (" Accept: application/json" );
146+ }
147+
148+ public:
149+ /* *
150+ * @brief Constructor with configuration
151+ */
152+ explicit Client (Config config)
153+ : config_(std::move(config))
154+ , rate_limiter_(
155+ config_.get_max_requests_per_minute(),
156+ config_.is_rate_limiting_enabled()
157+ ) {
158+ config_.validate ();
159+ }
160+
161+ /* *
162+ * @brief Constructor with API key (uses default configuration)
163+ */
164+ explicit Client (const std::string& api_key)
165+ : Client(Config(api_key)) {}
166+
167+ /* *
168+ * @brief Constructor of environment variables
169+ */
170+ static Client from_environment () {
171+ return Client (Config::from_environment ());
172+ }
173+
174+ /* *
175+ * @brief Synchronous request to the Chat Completions API
176+ */
177+ ChatResponse chat (const ChatRequest& request) {
178+ auto response_json = execute_request_with_retry ([&](HttpClient& http_client) {
179+ prepare_headers (http_client);
180+
181+ std::string url = config_.get_base_url () + " /chat/completions" ;
182+ std::string body = request.to_json ().dump ();
183+
184+ return http_client.post (url, body);
185+ });
186+
187+ try {
188+ auto j = json::parse (response_json);
189+ return ChatResponse::from_json (j);
190+ } catch (const json::exception& e) {
191+ throw JsonParseError (" Failed to parse response: " + std::string (e.what ()));
192+ }
193+ }
194+
195+ /* *
196+ * @brief Asynchronous request to the Chat Completions API
197+ *
198+ * Returns std::future for non-blocking execution
199+ */
200+ std::future<ChatResponse> chat_async (const ChatRequest& request) {
201+ return std::async (std::launch::async, [this , request]() {
202+ return this ->chat (request);
203+ });
204+ }
205+
206+ /* *
207+ * @brief Streaming request to Chat Completions API
208+ *
209+ * A callback is called for each data chunk
210+ */
211+ void chat_stream (ChatRequest request, StreamCallback callback) {
212+ request.stream (true );
213+
214+ rate_limiter_.wait_if_needed ();
215+
216+ HttpClient http_client (config_);
217+ prepare_headers (http_client);
218+
219+ std::string url = config_.get_base_url () + " /chat/completions" ;
220+ std::string body = request.to_json ().dump ();
221+
222+ // Streaming requires more complex processing
223+ // In the simplified version, we use a regular POST and parse SSE
224+ std::string response = http_client.post (url, body);
225+
226+ // Parsing Server-Sent Events
227+ std::istringstream stream (response);
228+ std::string line;
229+ std::string data_buffer;
230+
231+ while (std::getline (stream, line)) {
232+ if (line.empty () && !data_buffer.empty ()) {
233+
234+ if (data_buffer.substr (0 , 6 ) == " data: " ) {
235+ std::string json_str = data_buffer.substr (6 );
236+
237+ if (json_str == " [DONE]" ) {
238+ break ;
239+ }
240+
241+ try {
242+ auto j = json::parse (json_str);
243+ StreamChunk chunk = StreamChunk::from_json (j);
244+ callback (chunk);
245+ } catch (const json::exception& e) {
246+ throw JsonParseError (" Failed to parse stream chunk: " + std::string (e.what ()));
247+ }
248+ }
249+ data_buffer.clear ();
250+ } else {
251+ data_buffer += line + " \n " ;
252+ }
253+ }
254+ }
255+
256+ /* *
257+ * @brief Getting the client configuration
258+ */
259+ const Config& get_config () const {
260+ return config_;
261+ }
262+
263+ /* *
264+ * @brief Getting a rate limiter (for management and monitoring)
265+ */
266+ RateLimiter& get_rate_limiter () {
267+ return rate_limiter_;
268+ }
269+
270+ const RateLimiter& get_rate_limiter () const {
271+ return rate_limiter_;
272+ }
273+ };
274+
275+ } // namespace perplexity
276+ #endif // PERPLEXITY_CPP_CLIENT_H
0 commit comments