Skip to content

Repository files navigation

LLMClient

统一的 C++26 LLM 客户端库(类 LiteLLM):用一套 OpenAI 风格的类型访问多家大模型厂商。基于 Hackerl/asyncio 协程(curl + libuv 网络栈)、beman.execution(std::execution,P2300)与 nlohmann/json。

特性

  • 统一 OpenAI 格式:一套 ChatRequest / ChatResponse / Message / Tool 类型,各 Provider 负责与厂商原生 wire format 互转。
  • 多厂商:内置 OpenAI、Anthropic、Gemini、Azure OpenAI,以及 DeepSeek / Moonshot / 通义千问 / Ollama 等 OpenAI 兼容端点;可注册任意自定义 OpenAI 兼容端点。
  • SSE 流式输出:统一的 StreamChunk(text delta / tool call delta / finish reason / usage)。
  • Tool Calling:工具定义(JSON Schema)、tool_calls 解析、tool_result 回传,跨厂商一致;ToolLoop 进一步自动驱动"模型 ↔ 工具"多轮循环。
  • 结构化输出ResponseFormat 统一封装 JSON mode / JSON Schema(Anthropic 经强制工具模拟)。
  • EmbeddingsClient::embedding / Router::embedding,统一 EmbeddingRequest / EmbeddingResponse;超出 batch_size 自动并发分批、按序合并。
  • 多模态输入Message::user_with_parts 组合文本 / 图片(ImagePart)/ 音频(AudioPart);图片支持 http(s) URL 与 base64 data URI,由各 Provider 转成原生 wire 格式。
  • 多模态生成Client::generate_image(dall-e / gpt-image / Imagen)与 Client::generate_speech(OpenAI TTS / Gemini TTS);Gemini File API 大文件上传(Client::upload_file)。
  • Realtime:WebSocket 传输 + OpenAI Realtime 事件级会话封装(llm::realtime::Session)。
  • Prompt cachingMessage::cache 断点标记(Anthropic 原生映射),Usage::cache_read/write_tokens 统一上报命中量。
  • Router:按模型重试(指数退避、遵循 Retry-After)+ 跨模型 fallback + 客户端侧 TPM/RPM 配额跳过。
  • 协作式取消Task::cancel()co_await 立即以 ErrorKind::Cancelled 失败;sender 桥已接 stop token。
  • 工程化:GCRA 限速器(per-endpoint)、结构化日志钩子(LogCallback)、LiteLLM 风格 cost 估算(estimate_cost_usd)。
  • 两套 API 风格:C++20 协程(co_await 返回 std::expected<T, ApiError>)与 std::execution sender 桥(sync_wait 阻塞调用)。
  • 统一错误类型:单一 ApiError 贯穿全库,按 HTTP 状态码归类,可判断是否可重试。

架构

┌─────────────────────────────────────────────────────────┐
│  Client (协程 API)            Router (重试 / fallback)    │
│  chat() / chat_stream()       chat() / embedding()       │
│  embedding() / generate_image() / generate_speech()      │
│  upload_file()              ToolLoop (工具循环)            │
│  realtime::Session (WebSocket)                           │
├─────────────────────────────────────────────────────────┤
│  Registry (模型串路由 / 凭证解析 / per-endpoint 超时限速)   │
│  "provider/model" -> EndpointConfig + Credentials        │
├─────────────────────────────────────────────────────────┤
│  Provider (纯转换层,无协程,可离线单测)                    │
│  OpenAI │ Anthropic │ Gemini │ Azure │ OpenAI-compatible │
├─────────────────────────────────────────────────────────┤
│  HttpTransport (可 mock 抽象)    ws::Connection (WebSocket)│
│  AsyncioTransport -> asyncio::http (curl + libuv)        │
│  SseParser (增量 SSE 解析)                                │
└─────────────────────────────────────────────────────────┘

  sender 桥:TaskSender -> beman::execution::sync_wait
  (在独立 worker 线程的事件循环中跑协程,结果经 set_value/set_error 回传)

  横切:cancellable(协作式取消)│ RateLimiter(GCRA 限速)
        │ LogCallback(结构化日志)│ CostRegistry(cost 估算)

构建

所有依赖由源码编译到 third_party/install,不污染系统环境。

# 1. 一次性构建第三方依赖(asyncio/curl/libuv/openssl/json 等)
./third_party/build.sh

# 2. 配置 + 编译(debug preset,示例默认 ON)
cmake --preset debug
cmake --build --preset debug -j

# 3. 跑测试
ctest --preset debug

如需关闭示例或测试:-DLLMCLIENT_BUILD_EXAMPLES=OFF / -DLLMCLIENT_BUILD_TESTS=OFF

要求:CMake ≥ 3.30,支持 C++26 的编译器(GCC 13+;asyncio 的 std::stacktrace 需要 libstdc++exp,CMake 会自动探测链接)。

快速上手

协程版

链接 asyncio::asyncio-main 后由它提供 main(),只需定义 asyncMain

#include <cstdio>
#include <asyncio/task.h>
#include <llm/client.h>

asyncio::task::Task<void> asyncMain(int argc, char *argv[]) {
    llm::Client client;

    llm::ChatRequest req;
    req.model = "openai/gpt-4o";
    req.messages = {
        llm::Message::system("You are a helpful assistant."),
        llm::Message::user("用一句话介绍 C++ 协程。"),
    };

    const auto result = co_await client.chat(std::move(req));
    if (!result) {
        std::fprintf(stderr, "error: %s\n", result.error().to_string().c_str());
        co_return;
    }
    std::printf("%s\n", result->message.content.c_str());
    co_return;
}

流式:

co_await client.chat_stream(req, [](const llm::StreamChunk &chunk) {
    std::fputs(chunk.text_delta.c_str(), stdout);
    std::fflush(stdout);
});

Embeddings

llm::EmbeddingRequest req;
req.model = "openai/text-embedding-3-small";
req.input = {"hello world", "你好,世界"};

const auto result = co_await client.embedding(std::move(req));
if (result)
    std::printf("vectors=%zu  dim=%zu\n",
                result->embeddings.size(), result->embeddings[0].size());

Router::embeddingRouter::chat 一样提供按模型重试与 fallback。

多模态输入(图片 / 音频)

llm::ChatRequest req;
req.model = "openai/gpt-4o";
req.messages = {llm::Message::user_with_parts({
    std::string{"描述这张图片。"},
    llm::ImagePart{.url = "https://example.com/cat.jpg"},
    // 本地图片改用 base64:llm::ImagePart::from_base64("image/png", base64_data)
})};

const auto result = co_await client.chat(std::move(req));

std::execution sender 版(普通 main,阻塞)

#include <beman/execution/execution.hpp>
#include <llm/client.h>
#include <llm/sender.h>

int main() {
    llm::Client client;
    llm::ChatRequest req;
    req.model = "deepseek/deepseek-chat";
    req.messages = {llm::Message::user("Hi")};

    try {
        // 返回 std::optional<std::tuple<ChatResponse>>
        auto result = beman::execution::sync_wait(llm::chat_sender(client, std::move(req)));
        if (!result) { /* stopped */ return 1; }
        const auto &[resp] = *result;
        std::printf("%s\n", resp.message.content.c_str());
    }
    catch (const llm::ApiError &err) {
        // sync_wait 会把错误通道作为异常重新抛出
        std::fprintf(stderr, "request failed: %s\n", err.to_string().c_str());
    }
}

llm::chat_sync(client, req) 是上述两步的简写。

更多完整程序见 examples/basic_chatstreamingtool_callingtool_loopstructured_outputimage_generationrealtimerouter_fallbackembeddingmultimodal_chatsender_sync_wait(构建产物在 build/debug/examples/)。

模型串规则

ChatRequest::model 采用 LiteLLM 风格:provider/model;不带 / 的裸模型名默认走 OpenAI。内置端点:

前缀 端点 API key 环境变量 备注
openai https://api.openai.com/v1 OPENAI_API_KEY 默认 provider
anthropic https://api.anthropic.com ANTHROPIC_API_KEY 未指定时 max_tokens 默认 4096
gemini https://generativelanguage.googleapis.com GEMINI_API_KEY
azure 由环境变量提供 AZURE_OPENAI_API_KEY AZURE_OPENAI_ENDPOINTAZURE_OPENAI_API_VERSION 默认 2024-10-21;模型名为部署名
deepseek https://api.deepseek.com DEEPSEEK_API_KEY OpenAI 兼容
moonshot https://api.moonshot.cn/v1 MOONSHOT_API_KEY OpenAI 兼容
qwen https://dashscope.aliyuncs.com/compatible-mode/v1 DASHSCOPE_API_KEY OpenAI 兼容
ollama http://localhost:11434/v1 无需 key OpenAI 兼容,本地

环境变量未设置时 Registry::resolve 返回 ApiError{ErrorKind::Auth}ChatRequest 也支持按请求覆盖:api_key / base_url / timeout

自定义 OpenAI 兼容端点

llm::Registry::instance().register_openai_compatible(
    "groq",                             // 模型串前缀 groq/...
    "https://api.groq.com/openai/v1",
    "GROQ_API_KEY"                      // 环境变量名;空串表示无需 key
);

// 之后即可 req.model = "groq/llama-3.3-70b-versatile";

更底层的需求(非 OpenAI 线格式)可实现 llm::Provider 接口后用 register_endpoint 注册。

Router 配置

llm::Router router{
    client,
    llm::RouterConfig{
        .models = {"openai/gpt-4o", "deepseek/deepseek-chat"}, // 按序尝试
        .retry = {
            .max_retries = 2,                 // 每个模型首次失败后的重试次数
            .initial_delay = std::chrono::milliseconds{500},
            .max_delay = std::chrono::milliseconds{8000},
            .backoff_factor = 2.0,
        },
        .fallback_on_non_retryable = true,     // 认证/参数错误也切下一个模型
        // 可选:客户端侧配额,窗口内耗尽的模型直接跳过
        .model_limits = {{"openai/gpt-4o", {.rpm = 500, .tpm = 2000000}}},
    },
};

const auto result = co_await router.chat(req); // req.model 被忽略

行为:同一模型上,可重试错误(限流 / 5xx / 超时 / 网络错误)按指数退避重试,并遵循响应的 Retry-After;重试耗尽后切到下一个模型。不可重试错误(认证失败、请求非法等)是否立即 fallback 由 fallback_on_non_retryable 控制(默认 true)。全部模型失败时返回最后一个错误。

错误类型

所有 API 的失败统一为 llm::ApiError(协程版经 std::expected,sender 版经 set_error),to_string() 输出单行描述。

ErrorKind 含义 对应 HTTP
Auth 认证失败 / 缺 key 401 / 403
RateLimit 限流(附 retry_after 429
InvalidRequest 请求非法 / 未知 provider 400 / 422
NotFound 模型或路径不存在 404
Server 服务端错误 5xx
Timeout 连接 / 总超时
Network DNS / TLS / 连接失败
Parse 响应载荷无法解析
Cancelled 调用方主动取消(Task::cancel() / stop token)
Unknown 其他

llm::retryable(kind) 判断某类错误是否值得重试。

限制说明

  • Anthropic 无 embeddings API,也不支持音频输入:相应请求返回 ErrorKind::InvalidRequest
  • OpenAI embeddings 仅解析 encoding_format = "float"(默认)的响应;"base64" 暂不支持。
  • Gemini embeddings 响应不含 token 统计,EmbeddingResponse::usage 为 0。
  • 图片输入仅支持 http(s) URL 与 base64 data URI 两种形式(ImagePart::from_base64 生成后者)。
  • 流式工具调用按厂商增量事件合并;Gemini 流没有显式结束标记,按 EOF 处理。
  • Anthropic 的 system 消息从 messages 中抽出单独传递;max_tokens 必填,未指定时默认 4096。
  • 单请求超时:默认 600s,可用 ChatRequest::timeout 覆盖。

目录结构

include/llm/            公共头文件(types / error / provider / registry /
                        client / router / sender / http / sse / toolloop /
                        cancellable / ratelimit / logging / cost / ws / realtime)
include/llm/providers/  各厂商 Provider 头文件
src/                    库实现(src/providers/ 为各厂商实现)
examples/               示例程序(协程版 ×10 + sender 版 ×1)
tests/                  单元测试
third_party/            第三方依赖(build.sh 源码编译到 install/)
CMakeLists.txt          根构建脚本(LLMCLIENT_BUILD_EXAMPLES / _TESTS 开关)
CMakePresets.json       debug 等构建 preset

About

Unified C++26 LLM client library

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages