feat: add OpenAI-compatible AI provider plugin with SSE streaming support

- Implemented the OpenAI-compatible AI provider plugin, including configuration, chat, and chat_stream functionalities.
- Added support for SSE streaming and tool calls.
- Integrated Boost.JSON for JSON handling.
- Created CMake configuration for the plugin.
- Added error handling and logging throughout the plugin.
This commit is contained in:
2026-05-31 05:37:04 +08:00
parent f6cb51b40a
commit ba7382db2a
61 changed files with 163 additions and 147 deletions

View File

@@ -0,0 +1,20 @@
# ============================================================
# plugin-openai — OpenAI 兼容 AI 服务 / OpenAI-compatible AI service
# ============================================================
find_package(Boost REQUIRED CONFIG)
add_library(plugin-openai SHARED
src/openai_plugin.cpp
)
target_link_libraries(plugin-openai PRIVATE dstalk)
# Boost.JSON (header-only)
target_link_libraries(plugin-openai PRIVATE boost::boost dstalk_boost_config)
set_target_properties(plugin-openai PROPERTIES
PREFIX ""
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins
)

View File

@@ -0,0 +1,691 @@
/*
* @file openai_plugin.cpp
* @brief OpenAI-compatible AI provider plugin with SSE streaming and tool calls.
* DeepSeek/OpenAI 兼容 AI 提供者插件,支持 SSE 流式输出和工具调用。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include "dstalk/dstalk_host.h"
#include "dstalk/dstalk_services.h"
#include <boost/json.hpp>
#include <boost/json/src.hpp>
#include <atomic>
#include <cstring>
#include <string>
#include <vector>
namespace json = boost::json;
// ============================================================================
// 全局指针:从 on_init 获取W14.3: atomic acquire/release 保护读写竞态) / Global pointers: obtained from on_init (W14.3: atomic acquire/release protects read/write races)
// ============================================================================
static std::atomic<const dstalk_host_api_t*> g_host{nullptr};
static std::atomic<dstalk_http_service_t*> g_http{nullptr};
static std::atomic<dstalk_config_service_t*> g_config{nullptr};
// ============================================================================
// 配置数据(由 configure() 设置) / Config data (set by configure())
// ============================================================================
struct PluginConfig {
std::string provider;
std::string base_url;
std::string api_key;
std::string model;
int max_tokens = 4096;
double temperature = 0.7;
};
static PluginConfig g_cfg;
static std::string g_tools_json; // W20.2: 由 configure() 缓存,供 chat/chat_stream 使用 / cached by configure(), consumed by chat/chat_stream
// ============================================================================
// 安全擦除:用 volatile 写零循环防止编译器优化 / Secure erase: write zero loop through volatile to prevent compiler optimization
// ============================================================================
// 通过 volatile 写入零来安全擦除内存,防止编译器优化 / Securely zero out memory by writing through volatile to prevent compiler optimization.
static void secure_zero(void* p, size_t n) {
volatile char* vp = (volatile char*)p;
while (n--) *vp++ = 0;
}
// ============================================================================
// 辅助:从 base_url 提取 host 和 target / Helper: extract host and target from base_url
// ============================================================================
// 将 URL 解析为 scheme、host、port 和 target path 组件 / Parse a URL into scheme, host, port, and target path components.
static bool extract_host_port(const std::string& url,
std::string& scheme_out, std::string& host_out,
std::string& port_out, std::string& target_out)
{
size_t scheme_end = url.find("://");
if (scheme_end == std::string::npos) return false;
scheme_out = url.substr(0, scheme_end);
std::string rest = url.substr(scheme_end + 3);
size_t slash = rest.find('/');
std::string authority = (slash != std::string::npos) ? rest.substr(0, slash) : rest;
target_out = (slash != std::string::npos) ? rest.substr(slash) : "/";
size_t colon = authority.rfind(':');
if (colon != std::string::npos) {
host_out = authority.substr(0, colon);
port_out = authority.substr(colon + 1);
} else {
host_out = authority;
port_out = (scheme_out == "https") ? "443" : "80";
}
return true;
}
// ============================================================================
// 辅助:构建 headers JSON 字符串 / Helper: build headers JSON string
// ============================================================================
// 构建包含 Bearer 授权令牌的 JSON headers 对象 / Build the JSON headers object containing the Bearer authorization token.
static std::string build_headers_json(const std::string& auth_header_value)
{
json::object h;
h["Authorization"] = "Bearer " + auth_header_value;
return json::serialize(h);
}
// ============================================================================
// 辅助dstalk_message_t[] -> boost::json::array / Helper: dstalk_message_t[] -> boost::json::array
// ============================================================================
// 将 dstalk_message_t 数组转换为 Boost.JSON 数组,用于 API 请求体 / Convert dstalk_message_t array into a Boost.JSON array for the API request body.
static void append_history(json::array& msgs,
const dstalk_message_t* history, int history_len)
{
for (int i = 0; i < history_len; ++i) {
const auto& m = history[i];
json::object obj;
obj["role"] = m.role ? m.role : "";
if (m.role && std::strcmp(m.role, "tool") == 0) {
obj["tool_call_id"] = m.tool_call_id ? m.tool_call_id : "";
obj["content"] = m.content ? m.content : "";
} else if (m.role && std::strcmp(m.role, "assistant") == 0 &&
m.tool_calls_json && m.tool_calls_json[0] != '\0') {
obj["content"] = m.content ? m.content : "";
obj["tool_calls"] = json::parse(m.tool_calls_json);
} else {
obj["content"] = m.content ? m.content : "";
}
msgs.push_back(obj);
}
}
// ============================================================================
// 构建 DeepSeek JSON 请求体 / Build DeepSeek JSON request body
// ============================================================================
// 构建 DeepSeek/OpenAI chat completions API 的完整 JSON 请求体 / Build the full JSON request body for the DeepSeek/OpenAI chat completions API.
static std::string build_request_json(
const dstalk_message_t* history, int history_len,
const std::string& user_input,
const std::string& tools_json,
bool stream)
{
json::object root;
root["model"] = g_cfg.model;
root["max_tokens"] = g_cfg.max_tokens;
root["temperature"] = g_cfg.temperature;
root["stream"] = stream;
json::array msgs;
append_history(msgs, history, history_len);
// 追加当前用户输入 / Append current user input
if (!user_input.empty()) {
json::object obj;
obj["role"] = "user";
obj["content"] = user_input;
msgs.push_back(obj);
}
root["messages"] = msgs;
// tools 定义 / tools definition
if (!tools_json.empty()) {
root["tools"] = json::parse(tools_json);
}
return json::serialize(root);
}
// ============================================================================
// 解析非流式 JSON 响应 / Parse non-streaming JSON response
// ============================================================================
// 将非流式 JSON 响应体解析为 dstalk_chat_result_t / Parse a non-streaming JSON response body into a dstalk_chat_result_t.
static void parse_response(const dstalk_host_api_t* host,
const char* body, int http_status,
dstalk_chat_result_t& r)
{
r.http_status = http_status;
if (http_status < 200 || http_status >= 300) {
r.ok = 0;
try {
auto jv = json::parse(body ? body : "{}");
auto obj = jv.as_object();
if (obj.contains("error")) {
auto err = obj["error"].as_object();
r.error = host ? host->strdup(
json::value_to<std::string>(err["message"]).c_str()) : nullptr;
}
} catch (...) {
std::string msg = "HTTP " + std::to_string(http_status);
r.error = host ? host->strdup(msg.c_str()) : nullptr;
}
if (!r.error && host) {
std::string msg = "HTTP " + std::to_string(http_status);
r.error = host->strdup(msg.c_str());
}
r.content = nullptr;
r.tool_calls_json = nullptr;
return;
}
try {
auto jv = json::parse(body ? body : "{}");
auto obj = jv.as_object();
auto choices = obj["choices"].as_array();
if (!choices.empty()) {
auto msg = choices[0].as_object()["message"].as_object();
std::string content = json::value_to<std::string>(msg["content"]);
r.content = host ? host->strdup(content.c_str()) : nullptr;
if (msg.contains("tool_calls")) {
std::string tc = json::serialize(msg["tool_calls"]);
r.tool_calls_json = host ? host->strdup(tc.c_str()) : nullptr;
} else {
r.tool_calls_json = nullptr;
}
r.ok = 1;
r.error = nullptr;
} else {
r.ok = 0;
r.error = host ? host->strdup("empty response") : nullptr;
r.content = nullptr;
r.tool_calls_json = nullptr;
}
} catch (std::exception& e) {
r.ok = 0;
std::string msg = std::string("json parse: ") + e.what();
r.error = host ? host->strdup(msg.c_str()) : nullptr;
r.content = nullptr;
r.tool_calls_json = nullptr;
} catch (...) {
r.ok = 0;
r.error = host ? host->strdup("json parse error") : nullptr;
r.content = nullptr;
r.tool_calls_json = nullptr;
}
}
// ============================================================================
// 流式上下文:在 SSE 回调间累积内容和 tool_calls / Stream context: accumulate content and tool_calls across SSE callbacks
// ============================================================================
struct ToolCallAccum {
int index = -1;
std::string id;
std::string name;
std::string arguments; // 增量拼接的 JSON arguments 字符串 / incrementally concatenated JSON arguments string
};
struct StreamContext {
const dstalk_host_api_t* host;
dstalk_stream_cb user_cb;
void* userdata;
std::string accumulated;
bool streaming_ok = true;
std::vector<ToolCallAccum> tool_calls; // W20.2: 按 index 累积 delta tool_calls / accumulate delta tool_calls by index
};
// ============================================================================
// SSE 行解析OpenAI 兼容格式) / SSE line parsing (OpenAI-compatible format)
// ============================================================================
// 解析单行 SSE "data:" 行。如果包含 content delta将 token 写入 token_out。
// 如果包含 tool_calls delta累积到 ctx->tool_calls。
// 如果产生了 content token 则返回 true否则返回 falsetool_calls 或未知)。
// Parse a single SSE "data:" line. If it contains a content delta, writes the token
// to token_out. If it contains tool_calls delta, accumulates into ctx->tool_calls.
// Returns true if a content token was produced, false otherwise (tool_calls or unknown).
static bool parse_sse_line(const std::string& line, std::string& token_out,
StreamContext* ctx)
{
if (line.rfind("data: ", 0) != 0) return false;
std::string data = line.substr(6);
// F-13.2-3: 比较 [DONE] 哨兵前去除首尾空白 / Trim leading/trailing whitespace before comparing [DONE] sentinel.
const char* ws = " \t\r\n";
size_t start = data.find_first_not_of(ws);
if (start != std::string::npos) {
data.erase(0, start);
data.erase(data.find_last_not_of(ws) + 1);
}
if (data == "[DONE]") {
token_out.clear();
return true; // 流结束信号 / stream end signal
}
try {
auto jv = json::parse(data);
auto obj = jv.as_object();
auto choices = obj["choices"].as_array();
if (!choices.empty()) {
auto delta = choices[0].as_object()["delta"].as_object();
// W20.2: 处理 delta["tool_calls"] 增量 chunk / Handle delta["tool_calls"] incremental chunks
// DeepSeek/OpenAI 流式模式 tool_calls 跨多个 SSE 事件分片传输 / DeepSeek/OpenAI streaming mode: tool_calls transmitted across multiple SSE event chunks:
// 事件 1 / Event 1: {"index":0, "id":"call_xxx", "function":{"name":"foo"}}
// 事件 2 / Event 2: {"index":0, "function":{"arguments":"{\"bar\":"}}
// 事件 3 / Event 3: {"index":0, "function":{"arguments":"1}"}}
// 需要按 index 累积 id/name/arguments / Need to accumulate id/name/arguments by index.
if (delta.contains("tool_calls") && ctx) {
auto tc_array = delta["tool_calls"].as_array();
for (auto& tc_val : tc_array) {
auto tc_obj = tc_val.as_object();
int idx = tc_obj.contains("index")
? static_cast<int>(json::value_to<int64_t>(tc_obj["index"])) : -1;
if (idx < 0) continue;
while (static_cast<int>(ctx->tool_calls.size()) <= idx) {
ctx->tool_calls.push_back({});
}
auto& acc = ctx->tool_calls[idx];
acc.index = idx;
if (tc_obj.contains("id") && tc_obj["id"].is_string()) {
acc.id = json::value_to<std::string>(tc_obj["id"]);
}
if (tc_obj.contains("function") && tc_obj["function"].is_object()) {
auto func = tc_obj["function"].as_object();
if (func.contains("name") && func["name"].is_string()) {
acc.name = json::value_to<std::string>(func["name"]);
}
if (func.contains("arguments") && func["arguments"].is_string()) {
acc.arguments += json::value_to<std::string>(func["arguments"]);
}
}
}
return false; // tool_calls 已处理,无内容 token 给用户回调 / tool_calls processed, no content token for user callback
}
if (delta.contains("content")) {
token_out = json::value_to<std::string>(delta["content"]);
return true;
}
}
} catch (...) {
// 忽略解析失败 / Ignore parse failures
}
return false;
}
// ============================================================================
// configure 实现 / configure implementation
// ============================================================================
// 配置插件provider、endpoint、auth、model 和生成参数 / Configure the plugin with provider, endpoint, auth, model, and generation parameters.
static int my_configure(const char* provider, const char* base_url,
const char* api_key, const char* model,
int max_tokens, double temperature)
{
try {
if (provider) g_cfg.provider = provider;
if (base_url) g_cfg.base_url = base_url;
if (api_key) g_cfg.api_key = api_key;
if (model) g_cfg.model = model;
g_cfg.max_tokens = max_tokens;
g_cfg.temperature = temperature;
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host) {
// W20.2: 从 tools service 缓存 tools_json供 chat/chat_stream 复用 / Cache tools_json from tools service for reuse in chat/chat_stream
auto* tools_svc = reinterpret_cast<const dstalk_tools_service_t*>(
host->query_service("tools", 1));
if (tools_svc && tools_svc->get_tools_json) {
char* json = tools_svc->get_tools_json();
if (json) {
g_tools_json = json;
host->free(json);
}
}
host->log(DSTALK_LOG_INFO,
"[openai] configured: model=%s base_url=%s max_tokens=%d temperature=%.2f",
g_cfg.model.c_str(), g_cfg.base_url.c_str(),
g_cfg.max_tokens, g_cfg.temperature);
}
return 0;
} catch (const std::exception& e) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_configure exception: %s", e.what());
return -1;
} catch (...) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_configure unknown exception");
return -1;
}
}
// ============================================================================
// chat 实现 / chat implementation
// ============================================================================
// 非流式 chat completion发送 history + user input返回完整响应 / Non-streaming chat completion: send history + user input, return full response.
static dstalk_chat_result_t my_chat(
const dstalk_message_t* history, int history_len,
const char* user_input,
const char* tools_json)
{
try {
dstalk_chat_result_t r = {};
r.ok = 0;
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
dstalk_http_service_t* http = g_http.load(std::memory_order_acquire);
if (!http) {
r.error = host ? host->strdup("http service not available") : nullptr;
return r;
}
std::string scheme, host_name, port, target;
extract_host_port(g_cfg.base_url, scheme, host_name, port, target);
std::string target_path = target + "/chat/completions";
std::string body = build_request_json(history, history_len,
user_input ? user_input : "", tools_json ? tools_json : "", false);
std::string headers_json = build_headers_json(g_cfg.api_key);
char* response_body = nullptr;
int status_code = 0;
int ret = http->post_json(
host_name.c_str(), port.c_str(), target_path.c_str(), body.c_str(),
headers_json.c_str(), &response_body, &status_code);
if (ret != 0) {
r.error = host ? host->strdup("http request failed") : nullptr;
return r;
}
parse_response(host, response_body, status_code, r);
if (response_body) {
if (host) host->free(response_body);
}
return r;
} catch (const std::exception& e) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat exception: %s", e.what());
dstalk_chat_result_t r = {};
r.ok = 0;
r.error = host ? host->strdup(e.what()) : nullptr;
return r;
} catch (...) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat unknown exception");
dstalk_chat_result_t r = {};
r.ok = 0;
r.error = host ? host->strdup("unknown exception") : nullptr;
return r;
}
}
// ============================================================================
// chat_stream 实现 / chat_stream implementation
// ============================================================================
// 行回调:解析 SSE line将 token 传递给用户回调 / SSE line callback: parses each line and forwards content tokens to the user callback.
static int sse_line_callback(const char* line, void* userdata)
{
try {
auto* ctx = static_cast<StreamContext*>(userdata);
if (!line || !line[0]) return 1; // 空行,继续 / empty line, continue
std::string line_str(line);
std::string token;
if (!parse_sse_line(line_str, token, ctx)) return 1; // 非 data/tool_calls 行,继续 / not a data/tool_calls line, continue
if (token.empty()) return 0; // [DONE],停止 / [DONE], stop
ctx->accumulated += token;
if (ctx->user_cb) {
return ctx->user_cb(token.c_str(), ctx->userdata);
}
return 1; // 继续 / continue
} catch (const std::exception& e) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] sse_line_callback exception: %s", e.what());
return 0;
} catch (...) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] sse_line_callback unknown exception");
return 0;
}
}
// 流式 chat completion以 stream=true 发送 history + user input通过回调传递 token。
// 在 SSE 分片中累积 tool_calls 并在结束时序列化 / Streaming chat completion: send history + user input with stream=true, deliver tokens
// via callback. Accumulates tool_calls across SSE chunks and serializes them at end.
static dstalk_chat_result_t my_chat_stream(
const dstalk_message_t* history, int history_len,
const char* user_input,
dstalk_stream_cb cb, void* userdata)
{
try {
dstalk_chat_result_t r = {};
r.ok = 0;
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
dstalk_http_service_t* http = g_http.load(std::memory_order_acquire);
if (!http) {
r.error = host ? host->strdup("http service not available") : nullptr;
return r;
}
std::string scheme, host_name, port, target;
extract_host_port(g_cfg.base_url, scheme, host_name, port, target);
std::string target_path = target + "/chat/completions";
std::string body = build_request_json(history, history_len,
user_input ? user_input : "", g_tools_json, true);
std::string headers_json = build_headers_json(g_cfg.api_key);
StreamContext ctx;
ctx.host = host;
ctx.user_cb = cb;
ctx.userdata = userdata;
char* response_body = nullptr;
int status_code = 0;
int ret = http->post_stream(
host_name.c_str(), port.c_str(), target_path.c_str(), body.c_str(),
headers_json.c_str(),
sse_line_callback, &ctx,
&response_body, &status_code);
r.http_status = status_code;
// 检查传输层错误或非 2xx 状态 / Check transport errors or non-2xx status
if (status_code < 200 || status_code >= 300) {
r.ok = 0;
// 尝试从响应体提取错误信息 / Try to extract error info from response body
if (response_body && response_body[0]) {
try {
auto jv = json::parse(response_body);
auto obj = jv.as_object();
if (obj.contains("error")) {
auto err = obj["error"].as_object();
r.error = host ? host->strdup(
json::value_to<std::string>(err["message"]).c_str()) : nullptr;
}
} catch (...) {}
}
if (!r.error && host) {
if (status_code <= 0)
r.error = host->strdup("transport error");
else
r.error = host->strdup(
("HTTP " + std::to_string(status_code)).c_str());
}
if (response_body && host) host->free(response_body);
r.content = nullptr;
r.tool_calls_json = nullptr;
return r;
}
if (response_body && host) host->free(response_body);
// W20.2: 成功条件 = 有内容 OR 有 tool_callstool-only 响应如 function calling / Success = has content OR has tool_calls (tool-only responses like function calling)
bool has_content = !ctx.accumulated.empty();
bool has_tool_calls = !ctx.tool_calls.empty();
if (!has_content && !has_tool_calls) {
r.ok = 0;
r.error = host ? host->strdup("no content received") : nullptr;
r.content = nullptr;
r.tool_calls_json = nullptr;
} else {
r.ok = 1;
r.error = nullptr;
r.content = has_content
? host->strdup(ctx.accumulated.c_str()) : nullptr;
// 序列化累积的 tool_calls 为 JSON兼容 OpenAI tool_calls 格式) / Serialize accumulated tool_calls to JSON (OpenAI-compatible tool_calls format)
if (has_tool_calls) {
json::array tc_array;
for (auto& tc : ctx.tool_calls) {
json::object tc_obj;
tc_obj["index"] = tc.index;
if (!tc.id.empty()) tc_obj["id"] = tc.id;
tc_obj["type"] = "function";
json::object func;
if (!tc.name.empty()) func["name"] = tc.name;
func["arguments"] = tc.arguments;
tc_obj["function"] = func;
tc_array.push_back(std::move(tc_obj));
}
std::string tc_json = json::serialize(tc_array);
r.tool_calls_json = host ? host->strdup(tc_json.c_str()) : nullptr;
} else {
r.tool_calls_json = nullptr;
}
}
return r;
} catch (const std::exception& e) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat_stream exception: %s", e.what());
dstalk_chat_result_t r = {};
r.ok = 0;
r.error = host ? host->strdup(e.what()) : nullptr;
return r;
} catch (...) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat_stream unknown exception");
dstalk_chat_result_t r = {};
r.ok = 0;
r.error = host ? host->strdup("unknown exception") : nullptr;
return r;
}
}
// ============================================================================
// free_result 实现 / free_result implementation
// ============================================================================
// 释放 chat result 结构体中所有主机分配的字符串字段 / Free all host-allocated string fields in a chat result struct.
static void my_free_result(dstalk_chat_result_t* result)
{
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (!result || !host) return;
if (result->content) { host->free((void*)result->content); result->content = nullptr; }
if (result->error) { host->free((void*)result->error); result->error = nullptr; }
if (result->tool_calls_json) { host->free((void*)result->tool_calls_json); result->tool_calls_json = nullptr; }
}
// ============================================================================
// 服务 vtable / Service vtable
// ============================================================================
static dstalk_ai_service_t g_service = {
&my_configure,
&my_chat,
&my_chat_stream,
&my_free_result,
};
// ============================================================================
// 生命周期 / Lifecycle
// ============================================================================
// 插件初始化:查询 http 和 config 服务,注册 ai.openai 服务 / Plugin init: query http and config services, register ai.openai service.
static int on_init(const dstalk_host_api_t* host)
{
try {
dstalk_http_service_t* http = (dstalk_http_service_t*)host->query_service("http", 1);
dstalk_config_service_t* cfg = (dstalk_config_service_t*)host->query_service("config", 1);
g_host.store(host, std::memory_order_release);
g_http.store(http, std::memory_order_release);
g_config.store(cfg, std::memory_order_release);
if (!http) {
if (host) host->log(DSTALK_LOG_ERROR, "[openai] http service not found");
return -1;
}
if (host) host->log(DSTALK_LOG_INFO, "[openai] initializing OpenAI-compatible AI plugin");
return host->register_service("ai.openai", 1, &g_service);
} catch (const std::exception& e) {
const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire);
if (h && h->log) h->log(DSTALK_LOG_ERROR, "[openai] on_init exception: %s", e.what());
return -1;
} catch (...) {
const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire);
if (h && h->log) h->log(DSTALK_LOG_ERROR, "[openai] on_init unknown exception");
return -1;
}
}
// 插件关闭:从内存安全擦除 API key清空服务指针 / Plugin shutdown: securely erase API key from memory, null out service pointers.
static void on_shutdown()
{
try {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host) host->log(DSTALK_LOG_INFO, "[openai] shutdown");
secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size());
g_cfg.api_key.clear();
g_http.store(nullptr, std::memory_order_release);
g_config.store(nullptr, std::memory_order_release);
g_host.store(nullptr, std::memory_order_release);
} catch (const std::exception& e) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] on_shutdown exception: %s", e.what());
} catch (...) {
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] on_shutdown unknown exception");
}
}
// ============================================================================
// 插件描述符 / Plugin descriptor
// ============================================================================
static dstalk_plugin_info_t g_info = {
/* .name = */ "openai-compat",
/* .version = */ "1.0.0",
/* .description = */ "OpenAI-compatible AI provider (OpenAI-compatible API) / OpenAI-compatible AI 提供者 (OpenAI 兼容 API)",
/* .api_version = */ DSTALK_API_VERSION,
/* .dependencies = */ { "http", "config", NULL },
/* .on_init = */ on_init,
/* .on_shutdown = */ on_shutdown,
/* .on_event = */ nullptr,
};
// 必须入口点:返回插件描述符给主机 / Mandatory entry point: returns the plugin descriptor to the host.
extern "C" DSTALK_PLUGIN_EXPORT dstalk_plugin_info_t* dstalk_plugin_init(void)
{
return &g_info;
}