Skip to content

Commit 880f2f7

Browse files
committed
feat: added the main API client
1 parent 6a82c9c commit 880f2f7

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed

include/client.h

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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

Comments
 (0)