🔥小叶-duck:个人主页

 ❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已
择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、Gemini API 体系概述

  1.1 两套 API 设计机制与选型

    1.1.1 设计背景

    1.1.2 接入选型策略

  1.2 Gemini OpenAI 兼容 API 协议规范

    1.2.1 接口端点

    1.2.2 请求头规范

    1.2.3 请求体核心参数

二、Apifox 接口测试实操流程

  2.1 环境管理

  2.2 接口基础信息填写

  2.3 请求 Body(JSON)配置

  2.4 请求头配置

  2.5 网络代理配置

  2.6 发送请求,查看响应结果

三、GeminiProvider 头文件设计

  3.1 文件与依赖规范

  3.2 类接口标准声明

四、GeminiProvider 源文件核心实现

  4.1 模型初始化接口

    4.1.1 处理逻辑

    4.1.2 实现代码

  4.2 状态与元信息接口

    4.2.1 可用性检测

    4.2.2 模型描述获取

五、全量响应 sendMessage 实现

  5.1 检查模型是否有效

  5.2 构造并获取请求参数

  5.3 构造历史消息,组装 message 数组

  5.4 组装请求体 JSON

  5.5 JSON 序列化

  5.6 httplib 客户端初始化 + 代理环境变量处理

  5.7 设置请求头

  5.8 发送 POST 请求,校验返回状态

  5.9 解析响应体

  5.10 提取返回的响应内容

六、GTest 全量响应代码测试(TestLLM.cpp)

  6.1 CMakeLists.txt

  全量响应接口完整代码

结束语


前言

      在前序章节中,我们已经完成了 SDK 基础框架、日志组件、通用 LLMProvider 抽象基类的搭建,并且先后实现了 DeepSeek、ChatGPT 两个模型的接入逻辑,对 Chat Completions API、Responses API 两套接口的差异、流式 SSE 协议解析都有了比较完整的认知。

      本章我们将继续扩展这套可复用的 SDK 框架,完成 Gemini 模型的接入开发。Gemini 存在原生 API 与 OpenAI 兼容 API 两套不同的调用体系,二者鉴权方式、报文结构差异明显,因此在正式编码之前,我们会先梳理两套 API 的设计区别,明确本次项目选用兼容 OpenAI 接口的选型理由,依托网关抹平协议差异,最大限度复用已有抽象层与上层调用逻辑。

      内容会从接口协议规范、Apifox 预调试实操开始,再逐步完成 GeminiProvider 头文件声明、初始化、状态检测接口的实现,重点落地全量响应sendMessage接口,同时处理 Gemini 访问独有的代理配置、鉴权区别等工程问题;最后借助 GTest 编写测试用例、CMake 管理编译,验证整个链路的可用性。

一、Gemini API 体系概述

  1.1 两套 API 设计机制与选型

    1.1.1 设计背景

Gemini 对外提供两套完全不同的 API 体系,二者定位和发展路线差异很大:

  • OpenAI 兼容 API:属于厂商面向存量开发者的过渡方案,核心目的是降低迁移成本,让已经基于 OpenAI 技术栈开发的项目可以快速跑通 Gemini。但这套方案只是兼容层,长期依赖它会被限制在 OpenAI 的协议能力范围内,无法发挥 Gemini 独有的能力,厂商也很难建立属于自己的技术生态护城河。
  • 原生 API(models.generateContent):Google 主推的标准接口,完整释放 Gemini 多模态、工具调用、代码执行、实时检索等原生能力,适合垂直场景深度定制,是 Google 构建自身生态护城河的核心方案。

      从行业演进规律来看,很多大模型厂商都会遵循相似路径:早期以兼容 API 快速吸引开发者,站稳脚跟之后,再推出自有原生接口标准。

这里有一个很关键的对比,帮我们理解 DeepSeek 和 Gemini 策略上的区别:

  1. DeepSeek 快速跟进 Responses API:DeepSeek 的产品定位就是对 OpenAI 做无痛平替,极低迁移成本是它生态上的核心优势。只要 OpenAI 推出新接口标准(例如从 Chat Completions 演进到面向 Agent 场景的 Responses API),DeepSeek 会快速跟进兼容。开发者甚至不用修改 SDK 调用代码,只替换api_key和base_url,就可以在 ChatGPT 与 DeepSeek 之间切换。
  2. Gemini 的 OpenAI 兼容接口定位是 “敲门砖”:Google 和 OpenAI 属于直接竞争关系,并不希望 Gemini 仅仅作为 OpenAI 的替代品。它提供的/v1beta/openai/chat/completions只是一个最低限度的兼容入口,主要用来给存量旧系统做快速测试,吸引开发者体验 Gemini。 Google 重心维护的是自家 GenAI SDK 以及generateContent原生接口。Gemini 很多特色能力:实时搜索 Grounding、上下文缓存、原生音视频多模态、思维签名等,在 OpenAI 兼容协议里无法完整表达。Google 也没有跟进兼容 Responses API,这是我们后续编码实现时必须留意的重点。

    1.1.2 接入选型策略

  • 快速迁移、复用已有 OpenAI 代码:选用 OpenAI 兼容 API,只需要修改密钥、BaseURL、模型名称三处配置。
  • 需要使用 Gemini 多模态、长上下文、工具调用等特色能力:选用 Google 原生 API。
  • 本工程接入方案:优先使用 OpenAI 兼容 API 完成基础对话能力接入,降低开发成本;同时在代码架构上预留原生 API 的扩展入口,后续有需要可以平滑切换。

  1.2 Gemini OpenAI 兼容 API 协议规范

      Gemini 兼容层支持 chat/completions,而前面我们的 deepseek 正好也是通过 Chat Completions API 接入的,所以在 Gemini 在接口设计上和前面的 deepseek 整体是基本一致的,也就没有太多的注意事项了。

    1.2.1 接口端点

  • 请求方法:POST
  • 基准 URL:https://generativelanguage.googleapis.com
  • 接口路径:/v1beta/openai/chat/completions
  • 完整请求地址:https://generativelanguage.googleapis.com/v1beta/openai/chat/completions

    1.2.2 请求头规范

参数名类型取值规范
Content-Typestring固定值 application/json
Authorizationstring格式为 Bearer {GEMINI_API_KEY}

    1.2.3 请求体核心参数

参数名类型说明
modelstring模型标识,例如 gemini-3.6-flash
messagesarray对话消息列表,每条包含 role 和 content 字段
temperaturefloat采样温度,控制生成随机性,常用 0.7
max_tokensinteger单次生成最大 Token 上限
streamboolean是否开启流式响应,true 增量流式返回,false 全量一次性返回

请求体示例:

{
  "model": "gemini-3.6-flash",
  "messages": [
    {
      "role": "user",
      "content": "你是谁?"
    }
  ],
  "temperature": 0.7,
  "max_tokens": 2048,
  "stream": false
}

二、Apifox 接口测试实操流程

  2.1 环境管理

在 Apifox 中先完成环境变量配置,方便后续复用密钥与地址,也便于多环境切换。

  • 全局前置 URL:https://generativelanguage.googleapis.com
  • 环境变量:
    • GEMINI_API_KEY:存放 Gemini 的访问密钥,在 Apifox 内加密存储,避免明文写在请求里。
  • 环境隔离:可以区分开发、测试、正式、本地 Mock 多套环境,互不干扰。

  2.2 接口基础信息填写

      Gemini 兼容 OpenAI 的Chat Completions接口,不支持 Responses API,这一点是和 ChatGPT 最大的区别;报文结构和我们之前实现的 DeepSeek 保持一致。

  • 请求方式:POST
  • 完整地址:https://generativelanguage.googleapis.com/v1beta/openai/chat/completions
  • 拆分理解:
    • base_url:https://generativelanguage.googleapis.com/v1beta/openai
    • 接口路径:/chat/completions

  2.3 请求 Body(JSON)配置

      Body 选择 JSON 类型,核心参数如下:

{
  "model": "gemini-3.6-flash",
  "messages": [
    {
      "role": "user",
      "content": "你好,请简单介绍自己"
    }
  ],
  "temperature": 0.7,
  "max_tokens": 2048,
  "stream": false
}
  • stream: false:全量一次性返回;
  • stream: true:开启 SSE 流式增量返回。

  2.4 请求头配置

      在 Headers 标签页添加两项:

  1. Content-Type: application/json
  2. Authorization: Bearer {{GEMINI_API_KEY}}
    {{GEMINI_API_KEY}}会自动读取上面配置好的环境变量。

  2.5 网络代理配置

      和 OpenAI 的官方接口 一样,Gemini 官方接口也属于外网服务,需要代理网络才能访问,在 Apifox 的代理设置里单独配置:

  • 接口请求代理:仅 API 流量走代理,Apifox 本身网页访问不受影响
  • 自定义代理地址:填写代理 IP 和端口,示例:127.0.0.1:7890(本地代理工具常用默认端口,如果代理工具有指定端口则使用指定端口)
  • 代理绕过列表:配置不需要走代理的域名,英文逗号分隔

  2.6 发送请求,查看响应结果

① 全量响应(stream=false)

      返回结构是标准 Chat Completions 格式,顶层choices数组,message中存放完整回答内容,包含finish_reason、usage消耗统计等字段。这个结构也是后续我们实现全量响应代码解析响应体的参考。

{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "你好!我是由Google训练的大型语言模型……",
        "role": "assistant"
      }
    }
  ],
  "created": 1793514015,
  "id": "GdrmapNKJylqtsPoL7MQ",
  "model": "gemini-3.6-flash",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 164,
    "prompt_tokens": 7,
    "total_tokens": 862
  }
}

② 流式响应(stream=true)

      开启流式之后,服务端以 SSE 分片持续推送数据,每一条data:对应的 JSON 里面使用delta字段携带增量文本,没有额外事件类型区分,和 DeepSeek 的 Chat Completions 流式格式一致;流的末尾会单独推送一条 data: [DONE] 标记本次对话结束。
      这个结构也是后续我们实现流式响应代码解析响应体的参考。

{
  "choices": [
    {
      "delta": {
        "content": "你好!我是由 Google 训练的大型语言模型。你可以把我",
        "role": "assistant"
      },
      "index": 0
    }
  ],
  "created": 1793541724,
  "id": "WEbnaq2_GabVz7IPrsLN8Qw",
  "model": "gemini-3.6-flash",
  "object": "chat.completion.chunk"
}

三、GeminiProvider 头文件设计

  3.1 文件与依赖规范

  • 头文件路径:SDK/include/GeminiProvider.h
  • 源文件路径:SDK/src/GeminiProvider.cpp
  • 第三方依赖:jsoncpp(JSON 序列化与解析)、httplib(HTTP 网络请求)
  • 内部依赖:日志工具 myLog.h、抽象基类 LLMProvider.h、通用消息结构体定义

      设计思路和前面 DeepSeekProvider、ChatGPTProvider 保持统一,遵循同一个抽象契约,保证 SDK 框架可以无差别切换不同厂商大模型,这也是策略模式带来的扩展性。

  3.2 类接口标准声明

#ifndef GEMINI_PROVIDER_H
#define GEMINI_PROVIDER_H

#include "LLMProvider.h"
namespace ai_chat_sdk
{
// GeminiProvider模型提供者类:用于与Gemini模型交互
class GeminiProvider : public LLMProvider
{
  public:
    // 初始化模型:根据配置初始化模型,只接收全局连接配置(api_key、base_url)
    bool initModel(const std::unordered_map<std::string, std::string> &modelConfig) override;
    // 判断模型是否有效
    bool IsAvailable() override;
    // 获取模型描述
    std::string getModelDesc() override;

    // 发送消息:全量调用,返回完整响应
    // requestParams:针对Gemini模型本次请求专属参数,包含model、temperature、max_output_tokens等
    std::optional<std::string> sendMessage(const std::vector<Message> &messages,
                                           const std::unordered_map<std::string, std::string> &requestParams) override;

    // 发送消息:增量调用,实时返回响应
    // requestParams:针对Gemini模型本次请求专属参数,包含model、temperature、max_output_tokens等
    std::optional<std::string> sendMessageStream(const std::vector<Message> &messages,
                                                 const std::unordered_map<std::string, std::string> &requestParams,
                                                 std::function<void(const std::string &response, bool isEnd)> callback) override;

    // 析构函数:释放模型资源
    ~GeminiProvider() = default;
};
} // namespace ai_chat_sdk

#endif

四、GeminiProvider 源文件核心实现

  4.1 模型初始化接口

    4.1.1 处理逻辑

  1. 从配置 map 读取api_key字段,不存在或者为空字符串,打印错误日志,返回 false
  2. 从配置 map 读取base_url字段,不存在则打印错误日志,返回 false
  3. 两个参数校验通过,赋值给成员变量,标记_isAvailable = true,打印初始化成功日志,返回 true

    4.1.2 实现代码

#include "../include/GeminiProvider.h"
namespace ai_chat_sdk
{
// 初始化模型:根据配置初始化模型,只接收全局连接配置(api_key、base_url)
bool GeminiProvider::initModel(const std::unordered_map<std::string, std::string> &modelConfig)
{
    auto it = modelConfig.find("api_key");
    if (it == modelConfig.end() || it->second == "")
    {
        ERR("GeminiProvider initModel api_key not found or not config_error");
        return false;
    }
    else
    {
        _apikey = it->second;
    }

    it = modelConfig.find("base_url");
    if (it == modelConfig.end())
    {
        ERR("GeminiProvider initModel base_url not found");
        return false;
    }
    else
    {
        _baseUrl = it->second;
    }
    _isAvailable = true;
    INFO("GeminiProvider initModel success, apikey: {}, base_url: {}", _apikey, _baseUrl);
    return true;
}

代码分析:

  • 查找api_key的时候同时做两层判断:key 是否存在 + value 是否为空,防止拿到空密钥;
  • base_url只判断 key 是否存在,这里可以根据后续需求补充空值校验;
  • 校验全部通过后,打开可用标记_isAvailable,同时打印 INFO 日志,方便调试的时候确认配置加载情况;
  • 初始化只保存连接信息,不绑定模型名称、温度等参数,遵循我们之前统一的设计规范:连接配置属于 Provider 实例,推理参数属于单次请求。

  4.2 状态与元信息接口

    4.2.1 可用性检测

// 判断模型是否有效
bool GeminiProvider::IsAvailable()
{
    return _isAvailable;
}

      分析:直接返回内部布尔标记_isAvailable,上层调用方可以在发消息前先调用这个接口做前置校验,避免在未初始化成功的时候发起网络请求。

    4.2.2 模型描述获取

// 获取模型描述
std::string GeminiProvider::getModelDesc()
{
    return "gemini-3.6-flash 是 Google 专为 Agent 与复杂工作流优化的轻量主力模型,在保持极致低延迟和低成本的同时,提供前沿的编码、推理与全模态处理能力";
}

      分析:返回 Gemini 模型的介绍文本,和 DeepSeek、ChatGPT 的 getModelDesc 保持一样的输出格式,上层代码不需要修改,就可以打印任意一个 Provider 的信息,充分体现多态的价值。

五、全量响应 sendMessage 实现

  5.1 检查模型是否有效

// 1. 检查模型是否有效
if (!IsAvailable())
{
    ERR("GeminiProvider sendMessage model not available");
    return std::nullopt;
}

代码分析:
      
在发起网络请求之前,先做前置校验。IsAvailable() 读取内部标记 _isAvailable,判断当前 Provider 是否已经完成initModel初始化、密钥和地址是否就绪。
      如果没有初始化成功,直接打印错误日志,返回std::nullopt,避免无效的网络调用。

  5.2 构造并获取请求参数

// 2. 构造并获取请求参数
// 2.1 构造默认请求参数:如果requestParams中没有指定temperature、max_tokens,则使用默认值
double temperature = 0.7;
int max_tokens = 2048;
std::string model;

// 2.2 从requestParams中获取temperature、max_tokens参数;如果requestParams中指定了,则使用指定值
auto it = requestParams.find("temperature");
if (it != requestParams.end())
{
    temperature = std::stod(it->second);
}
it = requestParams.find("max_tokens");
if (it != requestParams.end())
{
    max_tokens = std::stoi(it->second);
}
it = requestParams.find("model");
if (it != requestParams.end())
{
    model = it->second;
}
else
{
    ERR("model name not found in requestParams, must config it");
    return std::nullopt;
}

代码分析

  • 先设置默认参数:temperature 默认 0.7、max_tokens 默认 2048,用户不传的时候自动兜底;
  • 从入参 requestParams(单次请求参数 map)读取参数,用户传入的值会覆盖默认值;
  • model 模型名称不设置默认值,强制要求调用方在每次请求时传入,缺失直接报错返回;
  • 符合我们整体框架设计:api_key/base_url 是实例初始化阶段的全局配置,model/temperature/max_tokens 属于单次推理参数,每次调用可以独立修改。

  5.3 构造历史消息,组装 message 数组

// 2.3 构造并获取历史消息,获取请求体中的 messages 这个json数组
Json::Value messageArray = Json::arrayValue;
for (auto &message : messages)
{
    Json::Value messageObj;
    messageObj["role"] = message._role;
    messageObj["content"] = message._content;
    messageArray.append(messageObj);
}

代码分析:
      
遍历上层传入的 std::vector<Message> 消息列表,循环把每一条消息转换成 JsonCpp 的 JSON 对象,放到数组里。

      注意:这里的 role/content 字段是适配 OpenAI 兼容格式的 Gemini 接入方式;原生 Gemini 接口字段名称和结构不一样,本项目选用兼容 OpenAI 的网关方式,降低多 Provider 之间的代码差异。

  5.4 组装请求体 JSON

// 3. 构造请求体
// 和历史消息构造一样,请求体的字段名称必须与gemini模型的接口文档中要求的一致
Json::Value requestBody;
requestBody["messages"] = messageArray;
requestBody["temperature"] = temperature;
requestBody["max_tokens"] = max_tokens;
requestBody["model"] = model;

代码分析:
      
把上面组装好的消息数组、温度、最大输出 token、模型名称,全部填入请求根 JSON 对象。
      这一步是报文组装的核心,字段必须和接口文档对齐,字段名写错会直接返回 400 参数错误。

  5.5 JSON 序列化

// 4. 对requestBody进行序列化
Json::StreamWriterBuilder writerbuilder;
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
INFO("GeminiProvider sendMessage requestBody: {}", requestBodyStr);

代码分析:
      
使用 JsonCpp 的StreamWriterBuilder将 Json::Value 序列化为字符串,同时打印完整请求报文日志,方便调试时排查参数错误。

  5.6 httplib 客户端初始化 + 代理环境变量处理

// 5. 使用cpp-httplib构造HTTP客户端
httplib::Client client(scheme_host_port, _baseUrl);

// 5.1 设置超时时间
client.set_read_timeout(sec(60), usec(0));    // 读取超时时间60秒
client.set_connection_timeout(sec(20), usec(0)); // 连接超时时间20秒

// 5.2 关键点:从环境变量中获取代理配置
char *proxy_env = std::getenv("MY_APP_PROXY");
if (proxy_env != nullptr)
{
    std::string proxy_str(proxy_env);
    auto pos = proxy_str.find(":");
    if (pos == std::string::npos)
    {
        ERR("GeminiProvider sendMessage proxy format error, must be host:port");
        return std::nullopt;
    }
    else
    {
        std::string proxy_host = proxy_str.substr(0, pos);
        int proxy_port = std::stoi(proxy_str.substr(pos + 1));
        client.set_proxy(proxy_host, proxy_port);
    }
}
else
{
    // 针对Gemini模型的特殊情况:如果用户没有配置代理环境变量,那请求无法访问到Gemini模型的服务器
    ERR("GeminiProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
    return std::nullopt;
}

代码分析:

  1. 创建 httplib 客户端,绑定提前保存的_baseUrl;设置连接超时、读取超时,防止网络卡死;
  2. Gemini 官方接口在国内无法直连,所以这里增加了强制代理校验:读取环境变量MY_APP_PROXY;
  3. 解析代理字符串,格式要求host:port,找不到冒号判定格式错误;解析成功后给 httplib 客户端设置代理;
  4. 重点区别于 DeepSeek 可以直连,不需要强制代理;Gemini 这里没有代理直接报错,是该 Provider 独有的业务约束。

  5.7 设置请求头

// 5.3 构建请求头
httplib::Headers headers = {
    {"Authorization", "Bearer " + _apikey}
};

      代码分析:
      
构造鉴权请求头,使用 Bearer 鉴权模式,和 OpenAI 体系保持一致,适配兼容 OpenAI 格式的 Gemini 网关。

      如果使用 Gemini 原生官方接口,鉴权头为 x-goog-api-key: {apikey},报文结构、字段命名和 OpenAI 体系差异很大,这就是两种接入方式的核心差异点。

配套原生接口 curl 示例:

curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \
  -H "x-goog-api-key: $GEMINI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-flash-latest",
    "input": "Hello, how are you?"
  }'

  5.8 发送 POST 请求,校验返回状态

// 6. 向指定服务器发送POST请求
auto resp = client.Post(path: "/v1beta/openai/chat/completions", headers, body: requestBodyStr, content_type: "application/json");
if (!resp)
{
    ERR("GeminiProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
    return std::nullopt;
}
if (resp->status != 200)
{
    ERR("GeminiProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
    return std::nullopt;
}
INFO("GeminiProvider sendMessage Post request success, status: {}, body: {}", resp->status, resp->body);

代码分析:

  1. 调用 Post 发送 JSON 报文,目标路径是兼容 OpenAI 的 /v1beta/openai/chat/completions;
  2. 第一层判断:!resp代表网络层面失败(连接失败、超时、ssl 错误),打印 httplib 内部错误;
  3. 第二层判断:status 不等于 200,代表服务端返回业务错误(密钥错误、参数非法、额度耗尽),打印响应体方便定位;
  4. 200 成功后打印完整响应报文日志,用于调试。

  5.9 解析响应体

// 7. 解析响应体 -- 反序列化
Json::CharReaderBuilder readerbuilder;
Json::Value responseBody;
std::stringstream responseStream(str::resp->body);
std::string errorMsg;
if (!Json::parseFromStream(readerbuilder, &responseStream, &responseBody, &errorMsg))
{
    ERR("GeminiProvider sendMessage parse responseBody failed, err: {}", errorMsg);
    return std::nullopt;
}

代码分析:
      
使用 JsonCpp 的CharReaderBuilder流式解析返回的 JSON 字符串;解析失败时拿到错误信息返回空,防止非法 JSON 导致程序异常。

  5.10 提取返回的响应内容

// 8. 从responseBody中提取响应内容
// 8.1 先检查 choices 是否存在且是数组且不为空 -- 保证我们能取到有数据的choices字段
if (responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty())
{
    Json::Value choice = responseBody["choices"][0];
    // 8.2 检查choices数组中是否存在message字段且message字段中是否存在content
    if (choice.isMember("message") && choice["message"].isObject() &&
        choice["message"].isMember("content") && choice["message"]["content"].isString())
    {
        std::string generatedText = choice["message"]["content"].asString();
        INFO("GeminiProvider sendMessage generatedText: {}", generatedText);
        return generatedText;
    }
    else
    {
        ERR("GeminiProvider: message or content field missing in response.");
        return std::nullopt;
    }
}
else
{
    ERR("GeminiProvider: choices array is empty or not found in response.");
    return std::nullopt;
}

代码分析:
      
按照 OpenAI 兼容格式的返回结构逐层校验:

  1. 判断顶层是否存在choices、是数组、数组不为空;
  2. 取第一个 choice 元素,再校验message对象、content字符串字段;
  3. 全部校验通过,取出 content 文本,返回给上层调用;任意一层字段缺失,打印对应错误日志,返回std::nullopt。

      多层防御性判断,避免响应结构和预期不一致时,JsonCpp 访问字段抛出异常、造成程序崩溃。

六、GTest 全量响应代码测试(TestLLM.cpp)

#include "../SDK/include/ChatGPTProvider.h"
#include "../SDK/include/DeepSeekProvider.h"
#include "../SDK/include/GeminiProvider.h"
#include "../SDK/include/OllamaLLMProvider.h"
#include <gtest/gtest.h>
#include <spdlog/common.h>

using namespace ai_chat_sdk;

TEST(GeminiProviderTest, sendMessage)
{
    // 1、创建GeminiProvider对象
    std::shared_ptr<LLMProvider> geminiProvider = std::make_shared<GeminiProvider>();
    // 1.1 检查GeminiProvider是否初始化成功
    ASSERT_TRUE(geminiProvider != nullptr);

    // 2、构建模型配置
    std::unordered_map<std::string, std::string> modelConfig;
    modelConfig["api_key"] = getenv("gemini_apikey") ? getenv("gemini_apikey") : "";
    modelConfig["base_url"] = "https://generativelanguage.googleapis.com";

    // 3、初始化GeminiProvider
    geminiProvider->initModel(modelConfig);
    // 3.1 检查模型是否有效
    ASSERT_TRUE(geminiProvider->IsAvailable());

    // 4、构建请求参数配置(不写则使用默认值)
    std::unordered_map<std::string, std::string> requestConfig;
    requestConfig["model"] = "gemini-3.6-flash";
    requestConfig["temperature"] = "0.7";
    requestConfig["max_tokens"] = "2048";

    // 5、构建消息列表
    std::vector<Message> messages;
    messages.push_back({"user", "你好, 请简单介绍一下你自己"});

    // 6、调用sendMessage发送消息
    auto response = geminiProvider->sendMessage(messages, requestConfig);
    // 6.1 检查响应是否为空
    ASSERT_TRUE(response.has_value());
    INFO("response: {}", response.value());
}

int main(int argc, char **argv)
{
    // 初始化Google Test框架
    testing::InitGoogleTest(&argc, argv);

    // 初始化日志系统
    Logger::logger_init("testLLM", "stdout", spdlog::level::info);

    // 运行所有测试用例
    return RUN_ALL_TESTS();
}

  6.1 CMakeLists.txt

# 设置Cmake最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(testLLM)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 要求必须使用C++17标准

# 设置构建类型为Debug
set(CMAKE_BUILD_TYPE Debug)

# 添加可执行文件
add_executable(testLLM TestLLM.cpp 
               ../SDK/src/DeepSeekProvider.cpp 
               ../SDK/src/util/MyLog.cpp 
               ../SDK/src/ChatGPTProvider.cpp 
               ../SDK/src/GeminiProvider.cpp)   

#设置生成可执行文件的输出目录
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
# ${CMAKE_BINARY_DIR} 表示生成可执行文件的所在目录,即cmake指令执行所在目录

# 添加头文件搜索路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../SDK/include)
# ${CMAKE_CURRENT_SOURCE_DIR} 表示当前CMakeLists.txt所在目录
# ${CMAKE_SOURCE_DIR} 表示项目根目录,即所有CMakeLists.txt所在路径中最外层目录

# 查找第三方库:确保在使用前已经通过find_package命令找到并配置了这些库
find_package(GTest REQUIRED)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(jsoncpp REQUIRED)
find_package(OpenSSL REQUIRED)

# 链接第三方库
target_link_libraries(testLLM gtest fmt spdlog jsoncpp pthread OpenSSL::SSL OpenSSL::Crypto)

      在 CMakeLists.txt 文件当中,只需要将 GeminiProvider 源文件的所在位置添加到add_executable当中即可。

测试结果展示:

  全量响应接口完整代码

// 发送消息:全量调用,返回完整响应
// requestParams:针对Gemini模型本次请求专属参数,包含model、temperature、max_output_tokens等
std::optional<std::string> GeminiProvider::sendMessage(const std::vector<Message> &messages,
                                                       const std::unordered_map<std::string, std::string> &requestParams)
{
    // 1. 检查模型是否有效
    if (!IsAvailable())
    {
        ERR("GeminiProvider sendMessage model not available");
        return std::nullopt;
    }

    // 2、构造并获取请求参数
    // 2.1 构造默认请求参数:如果requestParams中没有指定temperature、max_tokens,则使用默认值
    double temperature = 0.7;
    int max_tokens = 2048;
    std::string model;
    // 2.2 从requestParams中获取temperature、max_tokens参数:如果requestParams中指定了,则使用指定值
    auto it = requestParams.find("temperature");
    if (it != requestParams.end())
    {
        temperature = std::stod(it->second);
    }
    it = requestParams.find("max_tokens");
    if (it != requestParams.end())
    {
        max_tokens = std::stoi(it->second);
    }
    it = requestParams.find("model");
    if (it != requestParams.end())
    {
        model = it->second;
    }
    else
    {
        ERR("model name not found in requestParams, must config it");
        return std::nullopt;
    }

    // 2.3 构造并获取历史消息:获取请求体中的 messages 这个Json数组
    Json::Value messageArray(Json::arrayValue);
    for (auto &message : messages)
    {
        Json::Value messageObj;
        messageObj["role"] = message._role;
        messageObj["content"] = message._content;
        messageArray.append(messageObj);
    }

    // 3、构造请求体
    // 和历史消息构造一样,请求体的字段名称必须与gemini模型的接口文档中要求的一致
    Json::Value requestBody;
    requestBody["messages"] = messageArray;
    requestBody["temperature"] = temperature;
    requestBody["max_tokens"] = max_tokens;
    requestBody["model"] = model;

    // 4、对requestBody进行序列化
    Json::StreamWriterBuilder writerbuilder;
    std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
    INFO("GeminiProvider sendMessage requestBody: {}", requestBodyStr);

    // 5、使用cpp-httplib构建HTTP客户端
    httplib::Client client(_baseUrl);

    // 5.1设置超时时间
    client.set_read_timeout(60, 0);       // 读取超时时间60秒
    client.set_connection_timeout(20, 0); // 连接超时时间20秒
    // 5.2 关键点:从环境变量中获取代理配置
    char *proxy_env = std::getenv("MY_APP_PROXY");
    if (proxy_env != nullptr)
    {
        std::string proxy_str(proxy_env);
        auto pos = proxy_str.find(":");
        if (pos == std::string::npos)
        {
            ERR("GeminiProvider sendMessage proxy_env format error, must be host:port");
            return std::nullopt;
        }
        else
        {
            std::string proxy_host = proxy_str.substr(0, pos);
            int proxy_port = std::stoi(proxy_str.substr(pos + 1));
            client.set_proxy(proxy_host, proxy_port);
        }
    }
    else
    {
        // 针对Gemini模型的特殊情况:如果用户没有配置代理环境变量,那么请求无法访问到Gemini模型的服务器
        ERR("GeminiProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
        return std::nullopt;
    }

    // 5.3 构建请求头
    httplib::Headers headers = {
        {"Authorization", "Bearer " + _apikey}};

    // 6、向指定服务器发送POST请求
    auto resp = client.Post("/v1beta/openai/chat/completions", headers, requestBodyStr, "application/json");
    if (!resp)
    {
        ERR("GeminiProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
        return std::nullopt;
    }
    if (resp->status != 200)
    {
        ERR("GeminiProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
        return std::nullopt;
    }
    INFO("GeminiProvider sendMessage Post request success, status: {}, body: {}", resp->status, resp->body);

    // 7、解析响应体 -- 反序列化
    Json::CharReaderBuilder readerbuilder;
    Json::Value responseBody;
    std::stringstream responceStream(resp->body);
    std::string errorMsg;
    if (!Json::parseFromStream(readerbuilder, responceStream, &responseBody, &errorMsg))
    {
        ERR("GeminiProvider sendMessage parse responceBody failed, err: {}", errorMsg);
        return std::nullopt;
    }

    // 8、从responceBody中提取响应内容
    // 8.1 先检查 choices 是否存在且是数组且不为空 -- 保证我们能获取到有数据的choices字段
    if (responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty())
    {
        Json::Value choice = responseBody["choices"][0];
        // 8.2 检查choices数组中是否存在message字段且message字段中是否存在content
        if (choice.isMember("message") && choice["message"].isObject() &&
            choice["message"].isMember("content") && choice["message"]["content"].isString())
        {
            std::string generatedText = choice["message"]["content"].asString();
            INFO("GeminiProvider sendMessage generatedText: {}", generatedText);
            return generatedText;
        }
        else
        {
            ERR("GeminiProvider: message or content field missing in response.");
            return std::nullopt;
        }
    }
    else
    {
        ERR("GeminiProvider: choices array is empty or not found in response.");
        return std::nullopt;
    }
}

结束语

      到这里,Gemini 的全量响应接入开发与验证工作就全部完成了。我们先是区分了 Gemini 原生 API 和 OpenAI 兼容 API 两套体系,敲定了兼容网关的接入方案,依靠 Apifox 提前调通接口、确认报文规范;接着一步步完成 GeminiProvider 的类声明、基础接口实现,落地了全量 sendMessage 接口,处理了代理、鉴权这类和前面模型不一样的细节,最后通过 GTest 测试用例验证了整条调用链路可以正常工作。

      对比之前 DeepSeek、ChatGPT 的接入过程,也能直观感受到我们抽象基类 LLMProvider 的价值:新增一个模型厂商,只需要实现对应的子类,上层调用逻辑几乎不用改动,框架的扩展性得到了很好的体现。

Logo

更多推荐