
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已 择之路,纵是荆棘遍野,亦作花海遨游
目录
一、ChatGPTProvider.cpp:成员函数实现及解析
[1.1 断模型是否有效](#1.1 断模型是否有效)
[1.2 构造并获取请求参数](#1.2 构造并获取请求参数)
[1.2.1 设置参数默认值](#1.2.1 设置参数默认值)
[1.2.2 从 requestParams 读取用户传入参数](#1.2.2 从 requestParams 读取用户传入参数)
[1.2.3 组装历史消息 messages 数组](#1.2.3 组装历史消息 messages 数组)
[1.3 构造请求体](#1.3 构造请求体)
[1.4 对 requestBody 进行序列化](#1.4 对 requestBody 进行序列化)
[1.5 使用 cpp-httplib 构造 HTTP 客户端](#1.5 使用 cpp-httplib 构造 HTTP 客户端)
[1.5.1 设置超时时间](#1.5.1 设置超时时间)
[1.5.2 从环境变量中读取代理配置](#1.5.2 从环境变量中读取代理配置)
[1.5.3 构造请求头](#1.5.3 构造请求头)
[1.6 向指定服务器发送 POST 请求](#1.6 向指定服务器发送 POST 请求)
[1.7 解析响应体](#1.7 解析响应体)
[1.8 从 responseBody 中提取响应内容](#1.8 从 responseBody 中提取响应内容)
[二、GTest 全量响应代码测试(TestLLM.cpp)](#二、GTest 全量响应代码测试(TestLLM.cpp))
[2.1 CMakeLists.txt](#2.1 CMakeLists.txt)
[2.2 运行报错分析:400 unsupported_content_type 问题](#2.2 运行报错分析:400 unsupported_content_type 问题)
[2.2.1 报错现象](#2.2.1 报错现象)
[2.2.2 报错根本原因](#2.2.2 报错根本原因)
[2.2.3 疑问:同样写法,为什么 DeepSeek 调用没有报错?](#2.2.3 疑问:同样写法,为什么 DeepSeek 调用没有报错?)
[2.2.4 标准修复方案](#2.2.4 标准修复方案)
[三、全量接口 sendMessage 完整代码](#三、全量接口 sendMessage 完整代码)
前言
在上篇文章,我们梳理完 OpenAI 两套 API 的接口规范,理解了 Chat Completions 与新一代 Responses API 在报文结构、字段定义上的差异。本章我们将落地实现
ChatGPTProvider的全量响应sendMessage接口,基于LLMProvider统一抽象基类,完成对接 Responses API 的完整业务代码。我们会按代码执行流程逐段拆解每一处实现逻辑,包括模型可用性校验、请求参数解析、JSON 报文组装、httplib 客户端与代理配置、网络请求发送、返回 JSON 解析等环节。随后借助 GTest 编写测试用例,使用 CMake 进行编译构建,在联调过程中,我们会重点分析本次遇到的经典踩坑问题:
400 unsupported_content_type,搞清楚为什么相同的写法在 DeepSeek 接口可以正常调用,在 OpenAI 网关却直接报错,理解不同厂商网关的校验宽容度差异,给出一套能够兼容多家大模型服务商的规范写法。
一、ChatGPTProvider.cpp:成员函数实现及解析
本节我们实现 ChatGPTProvider 的全量响应 sendMessage 接口,该接口遵循我们项目统一的 LLMProvider 基类契约,对接 OpenAI 新一代 Responses API。整体逻辑按照:**"可用性校验 → 请求参数解析 → JSON 请求体组装 → HTTP 客户端配置与代理读取 → 发起 POST 请求 → 响应 JSON 解析 → 提取返回文本"**的顺序实现。
1.1 断模型是否有效
在发起任何网络请求之前,先调用 IsAvailable() 做前置校验,判断当前 Provider 是否已经完成初始化、密钥与基础地址是否配置正常。
- 如果模型不可用,直接打错误日志,返回
std::nullopt向上层告知调用失败; - 提前拦截无效调用,避免后续执行无效的网络请求,节约资源。
cpp
if (!IsAvailable())
{
ERR("ChatGPTProvider sendMessage model not available");
return std::nullopt;
}
1.2 构造并获取请求参数
参数分为两类:有默认值的模型控制参数,以及必须由调用方传入 的模型名称 model。
设计约定: api-key、baseUrl 属于所有模型通用配置,放在基类;model、temperature、max_output_tokens 属于单次请求参数,从入参
requestParams中读取。
1.2.1 设置参数默认值
如果调用方没有传入,我们给温度、最大输出 token 数设置默认值:
cpp
double temperature = 0.7;
int max_output_tokens = 2048;
std::string model;
1.2.2 从 requestParams 读取用户传入参数
遍历查询参数字典,用户传入则覆盖默认值; model 没有默认值,强制要求调用方传入,缺失直接报错返回。
cpp
auto it = requestParams.find("temperature");
if (it != requestParams.end())
{
temperature = std::stod(it->second);
}
it = requestParams.find("max_output_tokens");
if (it != requestParams.end())
{
max_output_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;
}
1.2.3 组装历史消息 messages 数组
遍历传入的对话消息列表 messages,按照 OpenAI Responses API 规范组装 JSON 数组,字段固定为 role、content,和接口文档对齐。
cpp
Json::Value messageArray(type::Json::arrayValue);
for (auto &message : messages)
{
Json::Value messageObject;
messageObject["role"] = message._role;
messageObject["content"] = message._content;
messageArray.append(messageObject);
}
1.3 构造请求体
按照 Responses API 的报文规范拼装顶层字段:model、temperature、max_output_tokens、input(input 对应我们组装好的消息数组)。
注意区分: 老版 Chat Completions 使用
messages字段,新一代 Responses API 使用input,这是两套 API 很关键的区别。
cpp
Json::Value requestBody;
requestBody["model"] = model;
requestBody["temperature"] = temperature;
requestBody["max_output_tokens"] = max_output_tokens;
requestBody["input"] = messageArray;
1.4 对 requestBody 进行序列化
使用 JsonCpp 将 Json::Value 对象序列化为字符串,作为 HTTP POST 的 body 载荷,同时打印日志方便调试,查看实际发送给 OpenAI 的请求结构。
cpp
Json::StreamWriterBuilder writerbuilder;
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
INFO("ChatGPTProvider sendMessage requestbody : {}", requestBodyStr);
1.5 使用 cpp-httplib 构造 HTTP 客户端
1.5.1 设置超时时间
设置读超时、连接超时,防止程序卡在网络阻塞,是生产网络请求必备的保护机制。
cpp
httplib::Client client(_baseUrl);
client.set_read_timeout(60, 0);
client.set_connection_timeout(20, 0);
1.5.2 从环境变量中读取代理配置
OpenAI 接口在国内直连无法访问,这里设计为从环境变量 MY_APP_PROXY 读取代理地址,格式强制要求 host:port。
- 读取环境变量,判断是否存在;
- 存在则拆分主机与端口,给 httplib 客户端设置代理;
- 环境变量不存在,直接报错返回,提示用户必须配置代理。
cpp
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("ChatGPTProvider sendMessage proxy_env format error, must be host:port");
return std::nullopt;
}
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
{
ERR("ChatGPTProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
return std::nullopt;
}
1.5.3 构造请求头
按照 OpenAI 认证规范,使用 Bearer Token 格式传递 api-key。
cpp
httplib::Headers headers = {
{"Authorization", "Bearer " + _apiKey},
{"Content-Type", "application/json"} //错误写法,后面会解释
};
1.6 向指定服务器发送 POST 请求
调用 httplib 的 Post 方法,访问 Responses 接口路径 /v1/responses,带上头、请求体;
两层判断:
第一层判断网络层面是否请求成功(连接失败、超时等网络错误);
第二层判断 HTTP 状态码是否等于 200。非 200 状态码代表业务侧报错(配额耗尽、密钥错误、模型不存在等),打印状态码与返回的错误报文,方便定位问题。
cpp
auto resp = client.Post("/v1/responses", headers, requestBodyStr, "application/json");
if (!resp)
{
ERR("ChatGPTProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
return std::nullopt;
}
if (resp->status != 200)
{
ERR("ChatGPTProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
return std::nullopt;
}
INFO("ChatGPTProvider sendMessage success, status: {}, body: {}", resp->status, resp->body);
1.7 解析响应体
利用 JsonCpp 的 CharReader 流式解析返回的 JSON 字符串,如果报文格式非法、JSON 解析失败,打日志并返回空。
cpp
Json::CharReaderBuilder readerbuilder;
Json::Value responseBody;
std::istringstream respBodyStream(resp->body);
std::string parse_err_msg;
if (!Json::parseFromStream(readerbuilder, &respBodyStream, &responseBody, &parse_err_msg))
{
ERR("ChatGPTProvider sendMessage parse responseBody failed, err_msg: {}", parse_err_msg);
return std::nullopt;
}
1.8 从 responseBody 中提取响应内容
按照 Responses API 返回的 JSON 结构逐层读取:
- 先判断顶层是否包含
output数组; - 取出
output[0]对象,校验包含content数组; - 取出
content[0]对象,读取text字段,这个就是大模型返回的回答文本; - 任意一层字段缺失,都打印对应的错误日志,返回
std::nullopt。
cpp
if (responseBody.isMember("output") && responseBody["output"].isArray() && !responseBody["output"].empty())
{
Json::Value outputObject = responseBody["output"][0];
if (outputObject.isMember("content") && outputObject["content"].isArray() && !outputObject["content"].empty())
{
Json::Value content = outputObject["content"][0];
if (content.isMember("text"))
{
std::string responseText = content["text"].asString();
INFO("ChatGPTProvider sendMessage responseText: {}", responseText);
return responseText;
}
else
{
ERR("ChatGPTProvider sendMessage responseBody content not found text field");
return std::nullopt;
}
}
else
{
ERR("ChatGPTProvider sendMessage responseBody output no content field");
return std::nullopt;
}
}
else
{
ERR("ChatGPTProvider sendMessage responseBody no output field");
return std::nullopt;
}

二、GTest 全量响应代码测试(TestLLM.cpp)
前置: GTest 第三方库在前面的文章中我们已经详细进行了讲解,感兴趣了解的可以看我之前的文章,这里是基于 TEST 宏写的单元测试,自动输出测试成功 / 失败信息。《C++ 工具库实战》GTest 单元测试:断言、全局环境、局部夹具与底层原理_gtest框架原理-CSDN博客
cpp
#include "../SDK/include/ChatGPTProvider.h"
#include "../SDK/include/DeepSeekProvider.h"
#include "../SDK/include/GeminiProvider.h"
#include <gtest/gtest.h>
#include <spdlog/common.h>
using namespace ai_chat_sdk;
TEST(ChatGPTProviderTest, sendMessage)
{
// 1、创建ChatGPTProvider对象
std::shared_ptr<LLMProvider> chatgptProvider = std::make_shared<ChatGPTProvider>();
// 1.1 检查ChatGPTProvider是否初始化成功
ASSERT_TRUE(chatgptProvider != nullptr);
// 2、构建模型配置
std::unordered_map<std::string, std::string> modelConfig;
modelConfig["api_key"] = getenv("chatgpt_apikey") ? getenv("chatgpt_apikey") : "";
modelConfig["base_url"] = "https://api.openai.com";
// 3、初始化ChatGPTProvider
chatgptProvider->initModel(modelConfig);
// 3.1 检查模型是否有效
ASSERT_TRUE(chatgptProvider->IsAvailable());
// 4、构建请求参数配置(不写则使用默认值)
std::unordered_map<std::string, std::string> requestConfig;
requestConfig["model"] = "gpt-4o-mini";
requestConfig["max_output_tokens"] = "2048";
requestConfig["temperature"] = "0.7";
// 5、构建消息列表
std::vector<Message> messages;
messages.push_back({"user", "你好, 请简单介绍一下你自己"});
// 6、调用sendMessage发送消息
auto response = chatgptProvider->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();
}
测试代码核心设计(和我们之前 SDK 设计保持一致)
- modelConfig(初始化配置) :api-key、base-url,属于这个 Provider 实例固定不变的属性,
initModel阶段一次性加载; - requestConfig(请求参数):model 名称、temperature、max_output_tokens 等,每次调用 sendMessage 都可以独立传参,同一个 Provider 实例可以用不同参数发请求;
- messages 数组 :维护会话上下文,体现大模型无状态特性,我们自己在 SDK 侧拼接历史消息,传给接口,模型依靠 messages 数组理解多轮对话;
- 返回值是
std::optional<std::string>:optional 区分「正常拿到回答」和「网络失败 / 报文解析失败 / 接口报错」两种场景,has_value()用来判断是否成功。
问题:为什么不直接硬编码 API Key?
API 密钥属于敏感凭证,如果直接写死在代码里,代码上传仓库会造成密钥泄露 。工程上常用环境变量的方式注入密钥。 操作步骤:
cpp
vim ~/.bashrc
# 在文件末尾添加
export chatgpt_apikey="你的ChatGPT密钥"
# 生效配置
source ~/.bashrc
- 执行
source ~/.bashrc之后,当前终端以及后续新开终端都能读取到这个环境变量; getenv("chatgpt_apikey")就是 C 标准库函数,读取系统环境变量的值;- 优势: 代码里不存放密钥,不同模型的 key 可以分开配置,切换模型时只改环境变量即可。
2.1 CMakeLists.txt
bash
# 设置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)
#设置生成可执行文件的输出目录
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 文件当中,只需要将 ChatGPTProvider 源文件的所在位置添加到add_executable当中即可。
2.2 运行报错分析:400 unsupported_content_type 问题

2.2.1 报错现象
我们执行 ChatGPTProvider 的全量响应测试用例时,接口返回 400 Bad Request,错误提示:
unsupported_content_type: "application/json, application/json". This API method only accepts 'application/json' requests, but you specified the header Content-Type: application/json, application/json
GTest 测试结果标记测试用例 Failure,has_value() 返回 false,调用失败。
核心问题:HTTP 请求里的 Content-Type 重复拼接,出现了双份 application/json。
2.2.2 报错根本原因
HTTP 规范 中,Content-Type 是单值请求头 ,一个 Header 只能携带一个 MIME 类型。 在我们原来的代码写法里出现两处同时设置 Content-Type:
- 在
httplib::Headers手动加入了一条{"Content-Type", "application/json"} - 在调用
client.Post()的最后一个入参,又传入了"application/json"
cpp-httplib 底层不会自动去重,会把两处的值用逗号拼接在一起,最终发给 OpenAI 网关的请求头变成:
bash
Content-Type: application/json, application/json
OpenAI 的网关校验严格,直接判定这是不符合 RFC 规范的非法请求,直接拒绝并返回 400。
2.2.3 疑问:同样写法,为什么 DeepSeek 调用没有报错?
这里不是代码逻辑有分支,而是不同厂商 API 网关的宽容度(容错设计)不一样,分为三点原因:
- 底层 Web 框架容错设计不同:
DeepSeek 这类国内大模型 API,后端大多基于 FastAPI、Gin、Flask 这类通用 Web 框架。这类框架在解析请求头时,只要逗号分隔的列表里面包含application/json,就会正常提取 body 的 JSON,忽略多余重复部分,属于 "宽松兼容"。
而 OpenAI 接入网关是自研高性能网关,严格遵循 RFC 标准,只要 Content-Type 是逗号分隔的复合类型,直接判定请求非法,拒绝解析报文。 - **Web 框架标准差异(Starlette 陷阱):**OpenAI 网关使用高标准校验逻辑,对同一个 Header 出现多个值的场景直接拦截;国内很多中转、API 服务为兼容老旧第三方 SDK,对这类不规范 Header 做了兼容处理。
- **安全防刷策略的区别(快速失败 Fail-fast):**OpenAI 面向全球公网,流量巨大,网关采用快速失败策略。只要请求格式不符合规范,直接拒绝,不继续解析请求体,减少服务器算力消耗。国内 API 服务压力模型不同,优先选择兼容不规范请求。

2.2.4 标准修复方案
**只保留一处设置 Content-Type,推荐删掉 Headers 里的 Content-Type,仅在 Post 接口参数中传递。**修改前后对比:
cpp
// 错误写法:两处同时配置
httplib::Headers headers = {
{"Authorization", "Bearer " + _apiKey},
{"Content-Type", "application/json"} // 这里多余
};
auto resp = client.Post(path, headers, requestBodyStr, "application/json");
// 正确写法:只在Post参数指定Content-Type
httplib::Headers headers = {
{"Authorization", "Bearer " + _apiKey}
};
auto resp = client.Post(path, headers, requestBodyStr, "application/json");
修改后,发出的请求头只会有一份合法的 Content-Type: application/json,既能通过 OpenAI 严格的网关校验,同时 DeepSeek 等宽松网关也可以正常识别,一套代码兼容所有模型服务商。
三、全量接口 sendMessage 完整代码
cpp
// 发送消息:全量调用,返回完整响应
std::optional<std::string> ChatGPTProvider::sendMessage(const std::vector<Message> &messages,
const std::unordered_map<std::string, std::string> &requestParams)
{
// 1、判断模型是否有效
if (!IsAvailable())
{
ERR("ChatGPTProvider sendMessage model not available");
// 模型无效,返回空值nullopt
return std::nullopt;
}
// 2、构造并获取请求参数
// 2.1 构造默认请求参数:如果requestParams中没有指定temperature、max_output_tokens,则使用默认值
double temperature = 0.7;
int max_output_tokens = 2048;
std::string model; // 请求参数model无法默认值,必须用户手动配置
// 2.2 从requestParams中获取temperature、max_output_tokens参数:如果requestParams中指定了,则使用指定值
// 注意:请求参数model在不同的模型中是必填项,但是这个参数由于模型版本的更新,可能会有不同的字段名称,所以就不能使用默认值了,需要用户自行指定正确的model
auto it = requestParams.find("temperature");
if (it != requestParams.end())
{
temperature = std::stod(it->second);
}
it = requestParams.find("max_output_tokens");
if (it != requestParams.end())
{
max_output_tokens = std::stoi(it->second);
}
// 请求参数model必须用户手动配置,如果没有配置则直接返回空值nullopt报错告诉用户
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 messageObject;
// 这里的字段名称必须与ChatGPT模型的接口文档中要求的一致
messageObject["role"] = message._role;
messageObject["content"] = message._content;
messageArray.append(messageObject);
}
// 3、构造请求体
// 和历史消息构造一样,请求体的字段名称必须与ChatGPT模型的接口文档中要求的一致
Json::Value requestBody;
requestBody["model"] = model;
requestBody["temperature"] = temperature;
requestBody["max_output_tokens"] = max_output_tokens;
requestBody["input"] = messageArray;
// 4、对requestBody进行序列化
Json::StreamWriterBuilder writerbuilder;
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
INFO("ChatGPTProvider sendMessage requestBody: {}", requestBodyStr);
// 5、使用cpp-httplib构建HTTP客户端
httplib::Client client(_baseUrl);
// 5.1 设置超时时间
client.set_read_timeout(60, 0);
client.set_connection_timeout(20, 0);
// 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("ChatGPTProvider 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
{
// 针对ChatGPT模型的特殊情况:如果用户没有配置代理环境变量,那么请求无法访问到ChatGPT模型的服务器
ERR("ChatGPTProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
return std::nullopt;
}
// 5.3 构建请求头
httplib::Headers headers = {
{"Authorization", "Bearer " + _apikey},
// {"Content-Type", "application/json"}
};
// 6、向指定服务器发送POST请求
auto resp = client.Post("/v1/responses", headers, requestBodyStr, "application/json");
if (!resp)
{
ERR("ChatGPTProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
return std::nullopt;
}
if (resp->status != 200)
{
ERR("ChatGPTProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
return std::nullopt;
}
INFO("ChatGPTProvider sendMessage success, status: {}, body: {}", resp->status, resp->body);
// 7、解析响应体
Json::CharReaderBuilder readerbuilder;
Json::Value responseBody;
std::istringstream respBodyStream(resp->body);
std::string parse_err_msg;
if (!Json::parseFromStream(readerbuilder, respBodyStream, &responseBody, &parse_err_msg))
{
ERR("ChatGPTProvider sendMessage parse responseBody failed, err_msg: {}", parse_err_msg);
return std::nullopt;
}
// 8、从responceBody中提取响应内容
// 8.1 检查responseBody是否包含output字段
if (responseBody.isMember("output") && responseBody["output"].isArray() && !responseBody["output"].empty())
{
// 8.2 检查output[0]是否包含content字段
Json::Value outputObject = responseBody["output"][0];
if (outputObject.isMember("content") && outputObject["content"].isArray() && !outputObject["content"].empty())
{
Json::Value content = outputObject["content"][0];
// 8.3 检查content[0]是否包含text字段
if (content.isMember("text"))
{
std::string responseText = content["text"].asString();
INFO("ChatGPTProvider sendMessage responseText: {}", responseText);
return responseText;
}
else
{
ERR("ChatGPTProvider sendMessage responseBody content not found text field");
return std::nullopt;
}
}
else
{
ERR("ChatGPTProvider sendMessage responseBody output no content field");
return std::nullopt;
}
}
else
{
ERR("ChatGPTProvider sendMessage responseBody no output field");
return std::nullopt;
}
}
结束语
到这里,ChatGPTProvider 全量响应 sendMessage 接口的代码实现、测试联调与问题修复就全部完成了。我们顺着代码执行链路逐段剖析了对接 OpenAI Responses API 的全部逻辑,同时在测试环节踩中了 cpp-httplib 请求头重复设置 Content-Type 的典型坑点,理清了不同厂商网关校验策略存在差异的底层原因,也得到了一套通用、规范的 HTTP 请求写法。
对比之前实现的 DeepSeekProvider,能够直观感受到这套 LLMProvider 抽象层的价值:上层调用接口保持一致,不同模型厂商、不同版本 API 之间的报文差异、协议细节全部收敛封装在子类内部。
本章我们完成的是非流式全量响应接口,后续我们会继续实现流式 SSE 版本的 sendMessageStream 接口,实现逐字返回的对话效果,补齐 ChatGPTProvider 的剩余能力。