Skip to content
Merged
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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,52 @@ int main() {
}
```

#### Using OpenAI-Compatible APIs (OpenRouter, etc.)

The OpenAI client can be used with any OpenAI-compatible API by specifying a custom base URL. This allows you to use alternative providers like OpenRouter, which offers access to multiple models through a unified API.

```cpp
#include <ai/openai.h>
#include <ai/generate.h>
#include <iostream>
#include <cstdlib>

int main() {
// Get API key from environment variable
const char* api_key = std::getenv("OPENROUTER_API_KEY");
if (!api_key) {
std::cerr << "Please set OPENROUTER_API_KEY environment variable\n";
return 1;
}

// Create client with OpenRouter's base URL
auto client = ai::openai::create_client(
api_key,
"https://openrouter.ai/api" // OpenRouter's OpenAI-compatible endpoint
);

// Use any model available on OpenRouter
auto result = client.generate_text({
.model = "anthropic/claude-3.5-sonnet", // or "meta-llama/llama-3.1-8b-instruct", etc.
.system = "You are a helpful assistant.",
.prompt = "What are the benefits of using OpenRouter?"
});

if (result) {
std::cout << result->text << std::endl;
}

return 0;
}
```

This approach works with any OpenAI-compatible API provider. Simply provide:
1. Your provider's API key
2. The provider's base URL endpoint
3. Model names as specified by your provider

See the [OpenRouter example](examples/openrouter_example.cpp) for a complete demonstration.

## Features

### Currently Supported
Expand Down Expand Up @@ -293,6 +339,7 @@ Check out our [examples directory](examples/) for more comprehensive usage examp
- [Basic Tool Calling](examples/tool_calling_basic.cpp)
- [Multi-Step Tool Workflows](examples/tool_calling_multistep.cpp)
- [Async Tool Execution](examples/tool_calling_async.cpp)
- [OpenRouter Integration](examples/openrouter_example.cpp) - Using OpenAI-compatible APIs


## Requirements
Expand Down
6 changes: 5 additions & 1 deletion examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ add_ai_example(retry_config_example retry_config_example.cpp)
add_ai_example(test_openai test_openai.cpp)
add_ai_example(test_anthropic test_anthropic.cpp)

# OpenRouter example (OpenAI-compatible)
add_ai_example(openrouter_example openrouter_example.cpp)

# Tool calling examples
add_ai_example(tool_calling_basic tool_calling_basic.cpp)
add_ai_example(tool_calling_multistep tool_calling_multistep.cpp)
Expand All @@ -55,13 +58,14 @@ add_subdirectory(components/all)
# Message to show how to run examples
message(STATUS "Examples will be built in: ${CMAKE_BINARY_DIR}/examples/")
message(STATUS "To run examples:")
message(STATUS " Set environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY")
message(STATUS " Set environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY")
message(STATUS " Run: ./examples/basic_chat")
message(STATUS " ./examples/streaming_chat")
message(STATUS " ./examples/multi_provider")
message(STATUS " ./examples/error_handling")
message(STATUS " ./examples/test_openai")
message(STATUS " ./examples/test_anthropic")
message(STATUS " ./examples/openrouter_example")
message(STATUS "")
message(STATUS "Component-specific examples:")
message(STATUS " ./examples/components/openai/openai_component_demo (OpenAI + Core only)")
Expand Down
90 changes: 90 additions & 0 deletions examples/openrouter_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include <cstdlib>
#include <iostream>
#include <string>

#include <ai/ai.h>
#include <ai/logger.h>

int main() {
try {
// Enable info logging
ai::logger::install_logger(std::make_shared<ai::logger::ConsoleLogger>(
ai::logger::LogLevel::kLogLevelInfo));

// Get OpenRouter API key from environment variable
const char* api_key_env = std::getenv("OPENROUTER_API_KEY");
if (!api_key_env) {
std::cerr << "Error: OPENROUTER_API_KEY environment variable not set\n";
std::cerr << "Please set your OpenRouter API key:\n";
std::cerr << " export OPENROUTER_API_KEY=your-api-key-here\n";
return 1;
}
std::string api_key(api_key_env);

// Create OpenAI client with OpenRouter's base URL
// OpenRouter provides an OpenAI-compatible API endpoint
auto client =
ai::openai::create_client(api_key, "https://openrouter.ai/api");

std::cout << "=== OpenRouter Example (OpenAI-compatible) ===\n\n";

// Test simple generation with a model available on OpenRouter
std::cout << "Testing text generation with OpenRouter...\n\n";

// Using a model that's available on OpenRouter
// Common models: "openai/gpt-3.5-turbo", "anthropic/claude-3.5-sonnet",
// "meta-llama/llama-3.1-8b-instruct" See https://openrouter.ai/models for
// available models
ai::GenerateOptions options(
"anthropic/claude-3.5-sonnet", "You are a helpful assistant.",
"What are the benefits of using OpenRouter for AI applications? Give a "
"brief answer.");

auto result = client.generate_text(options);

if (result) {
std::cout << "Response: " << result.text << "\n";
std::cout << "Model: " << result.model.value_or("unknown") << "\n";
std::cout << "Tokens used: " << result.usage.total_tokens << "\n";
std::cout << "Finish reason: " << result.finishReasonToString() << "\n";
} else {
std::cout << "Error: " << result.error_message() << "\n";
return 1;
}

// Test streaming with OpenRouter
std::cout << "\n\nTesting streaming with OpenRouter...\n";

ai::GenerateOptions stream_opts("anthropic/claude-3.5-sonnet",
"You are a creative writer.",
"Write a haiku about API compatibility.");
ai::StreamOptions stream_options(stream_opts);

auto stream = client.stream_text(stream_options);

std::cout << "Haiku:\n";
for (const auto& event : stream) {
if (event.is_text_delta()) {
std::cout << event.text_delta << std::flush;
} else if (event.is_error()) {
std::cout << "\nStream error: " << event.error.value_or("unknown")
<< "\n";
} else if (event.is_finish()) {
std::cout << "\n\nStream finished.\n";
if (event.usage.has_value()) {
std::cout << "Total tokens: " << event.usage->total_tokens << "\n";
}
}
}

std::cout << "\n=== Example completed successfully ===\n";
std::cout << "\nNote: You can use any model available on OpenRouter.\n";
std::cout << "Visit https://openrouter.ai/models for the full list.\n";

} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}

return 0;
}
32 changes: 20 additions & 12 deletions src/http/http_request_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,25 @@ HttpConfig HttpRequestHandler::parse_base_url(const std::string& base_url) {
HttpConfig config;
std::string url = base_url;

// Extract protocol and host
// Extract protocol
if (url.starts_with("https://")) {
config.host = url.substr(8);
url = url.substr(8);
config.use_ssl = true;
} else if (url.starts_with("http://")) {
config.host = url.substr(7);
url = url.substr(7);
config.use_ssl = false;
} else {
config.host = url;
config.use_ssl = true;
}

// Remove trailing slash and path if any
if (auto pos = config.host.find('/'); pos != std::string::npos) {
config.host = config.host.substr(0, pos);
// Extract host and path
auto pos = url.find('/');
config.host = (pos != std::string::npos) ? url.substr(0, pos) : url;
config.base_path = (pos != std::string::npos) ? url.substr(pos) : "";

// Remove trailing slash from base_path if present
if (!config.base_path.empty() && config.base_path.back() == '/') {
config.base_path.pop_back();
}

return config;
Expand Down Expand Up @@ -109,24 +113,28 @@ GenerateResult HttpRequestHandler::make_request(const std::string& path,
const std::string& content_type,
ResponseHandler handler) {
try {
ai::logger::log_debug(
"Making {} request to {}:{}{}", config_.use_ssl ? "HTTPS" : "HTTP",
config_.host, path, " with body size: " + std::to_string(body.size()));
// Combine base_path with the endpoint path
std::string full_path = config_.base_path + path;

ai::logger::log_debug("Making {} request to {}:{}{}",
config_.use_ssl ? "HTTPS" : "HTTP", config_.host,
full_path,
" with body size: " + std::to_string(body.size()));

if (config_.use_ssl) {
httplib::SSLClient cli(config_.host);
cli.set_connection_timeout(config_.connection_timeout_sec, 0);
cli.set_read_timeout(config_.read_timeout_sec, 0);
cli.enable_server_certificate_verification(config_.verify_ssl_cert);

auto res = cli.Post(path, headers, body, content_type);
auto res = cli.Post(full_path, headers, body, content_type);
return handler(res, "HTTPS");
} else {
httplib::Client cli(config_.host);
cli.set_connection_timeout(config_.connection_timeout_sec, 0);
cli.set_read_timeout(config_.read_timeout_sec, 0);

auto res = cli.Post(path, headers, body, content_type);
auto res = cli.Post(full_path, headers, body, content_type);
return handler(res, "HTTP");
}
} catch (const std::exception& e) {
Expand Down
2 changes: 2 additions & 0 deletions src/http/http_request_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace http {

struct HttpConfig {
std::string host;
std::string base_path; // Base path from URL (e.g., "/api" from
// "https://openrouter.ai/api")
bool use_ssl = true;
int connection_timeout_sec = 30;
int read_timeout_sec = 120;
Expand Down
32 changes: 23 additions & 9 deletions src/providers/openai/openai_stream.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "openai_stream.h"

#include "ai/logger.h"
#include "http/http_request_handler.h"

#include <chrono>
#include <mutex>
Expand All @@ -10,8 +11,6 @@ constexpr auto kEventTimeout = static_cast<std::chrono::seconds>(30);
constexpr auto kSleepInterval = std::chrono::milliseconds(1);
constexpr auto kConnectionTimeout = 30; // seconds
constexpr auto kReadTimeout = 300; // 5 minutes for long generations
const std::string kOpenAIHost = "api.openai.com";
const std::string kChatCompletionsPath = "/v1/chat/completions";
} // namespace

namespace ai {
Expand Down Expand Up @@ -93,14 +92,29 @@ void OpenAIStreamImpl::stop_stream() {
}
}

void OpenAIStreamImpl::run_stream(const std::string& /*url*/,
void OpenAIStreamImpl::run_stream(const std::string& url,
const httplib::Headers& headers,
const nlohmann::json& request_body) {
ai::logger::log_debug("Stream thread started - connecting to {}",
kOpenAIHost);
// Extract host and path from URL
std::string_view url_view(url);

// Skip protocol
if (auto pos = url_view.find("://"); pos != std::string_view::npos) {
url_view.remove_prefix(pos + 3);
}

// Split host and path
auto slash_pos = url_view.find('/');
std::string host(url_view.substr(0, slash_pos));
std::string path = (slash_pos != std::string_view::npos)
? std::string(url_view.substr(slash_pos))
: "/v1/chat/completions";

ai::logger::log_debug(
"Stream thread started - connecting to {} with path: {}", host, path);

try {
httplib::SSLClient client(kOpenAIHost);
httplib::SSLClient client(host);
client.enable_server_certificate_verification(true);
client.set_connection_timeout(kConnectionTimeout);
client.set_read_timeout(kReadTimeout);
Expand All @@ -114,14 +128,14 @@ void OpenAIStreamImpl::run_stream(const std::string& /*url*/,
// Create request
httplib::Request req;
req.method = "POST";
req.path = kChatCompletionsPath;
req.path = path;
req.headers = headers;
req.body = request_body.dump();
req.set_header("Content-Type", "application/json");

ai::logger::log_debug(
"Stream request prepared - path: {}, body size: {} bytes",
kChatCompletionsPath, req.body.length());
"Stream request prepared - path: {}, body size: {} bytes", path,
req.body.length());

// Set content receiver for streaming response
req.content_receiver = [this, &accumulated_data](
Expand Down