Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ add_library(gaia_core
src/console.cpp
src/clean_console.cpp
src/json_utils.cpp
src/http_client.cpp
src/lemonade_client.cpp
src/agent.cpp
src/mcp_client.cpp
Expand Down Expand Up @@ -306,6 +307,7 @@ if(GAIA_BUILD_TESTS)
tests/test_agent_vlm.cpp
tests/test_mcp_client.cpp
tests/test_console.cpp
tests/test_http_client.cpp
tests/test_lemonade_client.cpp
tests/test_clean_console.cpp
tests/test_tool_integration.cpp
Expand Down
7 changes: 5 additions & 2 deletions cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,16 @@ gaia/ # repo root
│ ├── tool_registry.h # Tool registration and execution
│ ├── mcp_client.h # MCP JSON-RPC client (stdio transport)
│ ├── json_utils.h # JSON extraction with multi-strategy fallback
│ ├── lemonade_client.h # HTTP client for the Lemonade inference server
│ ├── http_client.h # General HTTP/HTTPS client (GET/POST/streaming)
│ ├── lemonade_client.h # Lemonade inference server client (built on HttpClient)
│ ├── sse_parser.h # SSE parser for streaming chat completions
│ ├── console.h # TerminalConsole / SilentConsole output handlers
│ └── clean_console.h # CleanConsole — polished TUI with colors and word-wrap
├── src/
│ ├── agent.cpp # Agent loop state machine
│ ├── tool_registry.cpp
│ ├── lemonade_client.cpp # HTTP client (blocking + SSE streaming)
│ ├── http_client.cpp # HTTP transport (cpp-httplib behind a pimpl)
│ ├── lemonade_client.cpp # Lemonade client (blocking + SSE streaming)
│ ├── sse_parser.cpp # SSE token stream parser
│ ├── mcp_client.cpp # Cross-platform subprocess + pipes (Win32 / POSIX)
│ ├── json_utils.cpp
Expand All @@ -253,6 +255,7 @@ gaia/ # repo root
│ ├── test_agent.cpp
│ ├── test_tool_registry.cpp
│ ├── test_json_utils.cpp
│ ├── test_http_client.cpp
│ ├── test_lemonade_client.cpp
│ ├── test_sse_parser.cpp
│ ├── test_mcp_client.cpp
Expand Down
174 changes: 174 additions & 0 deletions cpp/include/gaia/http_client.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
//
// General-purpose HTTP client for GAIA C++ tools and agents.
//
// The transport (cpp-httplib) is a PRIVATE dependency compiled into gaia_core:
// this header must never include it, so consumers do not pay for a 10k-line
// header in every translation unit. All transport state lives behind a pimpl.

#pragma once

#include <cstddef>
#include <functional>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>

#include "gaia/export.h"

namespace gaia {

/// Request or response header map. Keys are matched case-insensitively when
/// merging and when reading (see HttpResponse::header()), and are sent with the
/// casing you supplied. Repeated response fields are comma-joined, so a
/// multi-value `Set-Cookie` is not round-trippable through this type.
using HttpHeaders = std::map<std::string, std::string>;

/// Streaming body callback. Return false to stop reading — that is treated as
/// normal completion, not an error (e.g. an SSE `[DONE]` sentinel).
using HttpChunkCallback = std::function<bool(const char* data, std::size_t len)>;

// ---- Response ----

struct GAIA_API HttpResponse {
int status = 0;
std::string body;
HttpHeaders headers;

bool ok() const { return status >= 200 && status < 300; }

/// Case-insensitive header lookup. Returns `fallback` when absent.
std::string header(const std::string& name, const std::string& fallback = "") const;
};

// ---- Error ----

/// Thrown for every HTTP failure: connection refused, timeout, TLS
/// unavailable, or a non-2xx status. There is no silent fallback — the client
/// never returns an empty or default response when a request fails.
///
/// what() names the method, the full URL, and the failure mode.
class GAIA_API HttpError : public std::runtime_error {
public:
HttpError(const std::string& message, std::string url, int status = 0,
std::string body = "");

/// Full URL of the failed request (scheme, host, port, path).
const std::string& url() const noexcept { return url_; }

/// HTTP status, or 0 when the request failed before a response arrived.
int status() const noexcept { return status_; }

/// Response body for a non-2xx status; empty otherwise. Streamed responses
/// buffer at most 512 bytes of it.
const std::string& body() const noexcept { return body_; }

private:
std::string url_;
int status_;
std::string body_;
};

// ---- Config ----

struct GAIA_API HttpClientConfig {
/// Base URL prepended to relative request paths, e.g.
/// "http://localhost:13305/api/v1". Joined to the path with exactly one
/// '/'; otherwise used verbatim (no /api/v1-style normalization).
std::string baseUrl;

/// Read timeout in seconds, must be > 0 (per-request override available).
int timeoutSec = 30;

/// Connection timeout in seconds, must be > 0 (per-request override available).
int connectTimeoutSec = 30;

/// Sent on every request; per-request headers of the same name win
/// (matched case-insensitively).
HttpHeaders defaultHeaders;

/// https only: verify the server certificate.
bool verifyTls = true;

/// https only: custom CA bundle path (empty → system trust store).
std::string caCertPath;

/// Log method and URL to stderr.
bool debug = false;
};

// ---- Client ----

/// Blocking HTTP/HTTPS client.
///
/// HttpClient http({"https://api.example.com"});
/// HttpResponse r = http.get("/v1/models", {{"Authorization", "Bearer …"}});
///
/// A request `path` may also be an absolute URL ("http://…" / "https://…"),
/// in which case the configured base URL is ignored.
///
/// HTTPS requires an OpenSSL-enabled build (auto-detected by CMake); an https
/// URL on an HTTP-only build throws HttpError rather than downgrading.
///
/// Not thread-safe: use one instance per thread.
class GAIA_API HttpClient {
public:
/// @throws std::invalid_argument if a configured timeout is not positive
explicit HttpClient(const HttpClientConfig& config = {});

/// Convenience constructor — base URL with default timeouts.
explicit HttpClient(const std::string& baseUrl);

~HttpClient();

HttpClient(const HttpClient&) = delete;
HttpClient& operator=(const HttpClient&) = delete;
HttpClient(HttpClient&&) noexcept;
HttpClient& operator=(HttpClient&&) noexcept;

/// GET `path`.
/// @param timeoutSec Read timeout override (0 → config value)
/// @param connectTimeoutSec Connect timeout override (0 → config value)
/// @throws HttpError on connection failure, timeout, or non-2xx status
HttpResponse get(const std::string& path, const HttpHeaders& headers = {},
int timeoutSec = 0, int connectTimeoutSec = 0);

/// POST `body` to `path`. Content-Type defaults to "application/json"
/// unless `headers` supplies one.
/// @throws HttpError on connection failure, timeout, or non-2xx status
HttpResponse post(const std::string& path, const std::string& body,
const HttpHeaders& headers = {}, int timeoutSec = 0,
int connectTimeoutSec = 0);

/// POST `body` to `path` and hand each response chunk to `onChunk` as it
/// arrives (SSE and other streamed responses).
///
/// The returned HttpResponse carries the status and headers; its body is
/// empty because the payload went to the callback. `onChunk` returning
/// false stops the read and completes normally. On a non-2xx status the
/// error body is buffered and reported through HttpError instead of being
/// passed to `onChunk`.
///
/// @throws HttpError on connection failure, timeout, or non-2xx status
HttpResponse postStreaming(const std::string& path, const std::string& body,
HttpChunkCallback onChunk,
const HttpHeaders& headers = {}, int timeoutSec = 0,
int connectTimeoutSec = 0);

const std::string& baseUrl() const;
void setBaseUrl(const std::string& url);

/// Add or replace a header sent on every request.
void setDefaultHeader(const std::string& name, const std::string& value);

bool debug() const;
void setDebug(bool enabled);

private:
struct Impl;
std::unique_ptr<Impl> impl_;
};

} // namespace gaia
31 changes: 11 additions & 20 deletions cpp/include/gaia/lemonade_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <nlohmann/json.hpp>

#include "gaia/export.h"
#include "gaia/http_client.h"
#include "gaia/types.h"

namespace gaia {
Expand Down Expand Up @@ -97,7 +98,7 @@ class GAIA_API LemonadeClient {
/// @param debug Emit extra diagnostics to stderr when true
LemonadeClient(const std::string& baseUrl, bool debug = false);

// Non-copyable (contains no resources but keep consistent with Agent)
// Non-copyable (owns an HttpClient; keeps consistency with Agent)
LemonadeClient(const LemonadeClient&) = delete;
LemonadeClient& operator=(const LemonadeClient&) = delete;

Expand Down Expand Up @@ -222,42 +223,32 @@ class GAIA_API LemonadeClient {
void setContextSize(int ctx) { contextSize_ = ctx; }

bool debug() const { return debug_; }
void setDebug(bool d) { debug_ = d; }
void setDebug(bool d) { debug_ = d; http_.setDebug(d); }

private:
/// Normalize URL: strip trailing slashes, preserve /v1 or /api/v1, append /api/v1 otherwise.
static std::string normalizeUrl(const std::string& url);

/// Parsed URL components.
struct UrlParts {
std::string host;
int port = 80;
std::string basePath; // everything after host:port (may be "")
bool useSSL = false;
};

UrlParts parseUrl(const std::string& url) const;

/// GET request; returns response body or throws.
/// GET request; returns response body or throws HttpError.
std::string httpGet(const std::string& path, int timeoutSec = 10);

/// POST request; returns response body or throws.
/// POST request; returns response body or throws HttpError.
std::string httpPost(const std::string& path, const std::string& body,
int timeoutSec = 30);

/// Streaming POST request using httplib::Client::send().
/// Calls receiver for each response body chunk. Sets streamDone=true when
/// receiver returns false (i.e. SseParser got [DONE]) so the caller can
/// distinguish intentional stream completion from a real cancellation error.
/// @throws std::runtime_error on connection error or non-2xx status
/// Streaming POST request. Calls receiver for each response body chunk;
/// receiver returning false ends the stream normally (i.e. SseParser got
/// [DONE]) rather than raising.
/// @throws HttpError on connection error or non-2xx status
void httpPostStreaming(const std::string& path, const std::string& body,
std::function<bool(const char*, size_t)> receiver,
bool& streamDone, int timeoutSec);
int timeoutSec);

std::string baseUrl_;
std::string model_;
int contextSize_ = 0;
bool debug_ = false;
HttpClient http_;
};

} // namespace gaia
Loading
Loading