Merge pull request #711 from Tokisaki-Galaxy/feat/llm-model-fetch
feat: AI 辅助新增获取模型、测试连接与 Custom 提供商
This commit is contained in:
1 parent
83a28afe2a
commit
aa5c632ea9
21 files changed
+699
-38
No files matched your search
@@ -20,6 +20,8 @@ namespace
|
||||
constexpr long kConnectTimeoutMs = 5000;
|
||||
constexpr long kRequestTimeoutMs = 15000;
|
||||
constexpr std::size_t kMaxResponseBytes = 256 * 1024;
|
||||
// OpenRouter 等聚合服务的 /models 带描述和价格,能到数 MB。
|
||||
constexpr std::size_t kMaxModelListBytes = 16 * 1024 * 1024;
|
||||
|
||||
struct HttpResponse
|
||||
{
|
||||
@@ -27,6 +29,7 @@ struct HttpResponse
|
||||
long status = 0;
|
||||
std::string body;
|
||||
std::string error;
|
||||
std::size_t max_bytes = kMaxResponseBytes;
|
||||
};
|
||||
|
||||
std::string Value(const ApiCredentialTest::Request &request, const char *key)
|
||||
@@ -38,10 +41,10 @@ std::string Value(const ApiCredentialTest::Request &request, const char *key)
|
||||
size_t WriteResponse(char *data, size_t size, size_t count, void *user)
|
||||
{
|
||||
const size_t bytes = size * count;
|
||||
auto *response = static_cast<std::string *>(user);
|
||||
if (bytes > kMaxResponseBytes - (std::min)(response->size(), kMaxResponseBytes))
|
||||
auto *response = static_cast<HttpResponse *>(user);
|
||||
if (bytes > response->max_bytes - (std::min)(response->body.size(), response->max_bytes))
|
||||
return 0;
|
||||
response->append(data, bytes);
|
||||
response->body.append(data, bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@@ -82,25 +85,35 @@ std::string ErrorDetail(const HttpResponse &response)
|
||||
return "HTTP " + std::to_string(response.status);
|
||||
}
|
||||
|
||||
HttpResponse PerformJsonPost(const std::string &endpoint, const std::string &token, const std::string &payload)
|
||||
// payload 为空指针时发 GET,否则以 JSON body 发 POST。
|
||||
HttpResponse PerformJsonRequest(const std::string &endpoint, const std::string &token, const std::string *payload,
|
||||
std::size_t max_bytes = kMaxResponseBytes)
|
||||
{
|
||||
InitCurl();
|
||||
HttpResponse response;
|
||||
response.max_bytes = max_bytes;
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl)
|
||||
return response;
|
||||
char error[CURL_ERROR_SIZE] = {};
|
||||
curl_slist *headers = nullptr;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = curl_slist_append(headers, payload ? "Content-Type: application/json" : "Accept: application/json");
|
||||
const std::string authorization = "Authorization: Bearer " + token;
|
||||
headers = curl_slist_append(headers, authorization.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str());
|
||||
NetworkProxy::ApplyToCurl(curl);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(payload.size()));
|
||||
if (payload)
|
||||
{
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload->c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(payload->size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteResponse);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, kRequestTimeoutMs);
|
||||
@@ -113,6 +126,26 @@ HttpResponse PerformJsonPost(const std::string &endpoint, const std::string &tok
|
||||
return response;
|
||||
}
|
||||
|
||||
// 由 Chat Completions 地址或 base URL 推出 OpenAI 兼容的 /models 地址。
|
||||
// 查询串(如 Azure 的 api-version)原样保留,只改写路径部分。
|
||||
std::string ModelListEndpoint(const std::string &endpoint)
|
||||
{
|
||||
const std::size_t query_at = endpoint.find_first_of("?#");
|
||||
std::string path = endpoint.substr(0, query_at);
|
||||
const std::string query = query_at == std::string::npos ? std::string() : endpoint.substr(query_at);
|
||||
while (!path.empty() && path.back() == '/')
|
||||
path.pop_back();
|
||||
const auto ends_with = [&path](std::string_view suffix) {
|
||||
return path.size() >= suffix.size() && path.compare(path.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
};
|
||||
constexpr std::string_view kChatSuffix = "/chat/completions";
|
||||
if (ends_with(kChatSuffix))
|
||||
path.resize(path.size() - kChatSuffix.size());
|
||||
if (!ends_with("/models"))
|
||||
path += "/models";
|
||||
return path + query;
|
||||
}
|
||||
|
||||
ApiCredentialTest::Result TestChat(const ApiCredentialTest::Request &request)
|
||||
{
|
||||
const std::string token = Value(request, "token");
|
||||
@@ -131,7 +164,8 @@ ApiCredentialTest::Result TestChat(const ApiCredentialTest::Request &request)
|
||||
body["thinking"] = {{"type", "disabled"}};
|
||||
else if (provider == "siliconflow")
|
||||
body["enable_thinking"] = false;
|
||||
const HttpResponse response = PerformJsonPost(endpoint, token, body.dump());
|
||||
const std::string payload = body.dump();
|
||||
const HttpResponse response = PerformJsonRequest(endpoint, token, &payload);
|
||||
if (response.code == CURLE_OK && response.status >= 200 && response.status < 300)
|
||||
return {true, "连接成功,API Key 和模型配置有效。"};
|
||||
return {false, "测试失败:" + ErrorDetail(response)};
|
||||
@@ -206,7 +240,7 @@ ApiCredentialTest::Result TestBatchAsr(const ApiCredentialTest::Request &request
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteResponse);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, kRequestTimeoutMs);
|
||||
@@ -277,4 +311,65 @@ Result Run(const Request &request)
|
||||
}
|
||||
return {false, "不支持的配置测试类型。"};
|
||||
}
|
||||
|
||||
ModelListResult FetchModels(const Request &request)
|
||||
{
|
||||
if (request.service != "ai.assistant")
|
||||
return {false, "不支持的模型列表类型。", {}};
|
||||
const std::string token = Value(request, "token");
|
||||
const std::string endpoint = Value(request, "endpoint");
|
||||
if (!CloudTranslation::IsUsableSecret(token))
|
||||
return {false, "请先填写有效的 API Key。", {}};
|
||||
if (!IsHttpEndpoint(endpoint))
|
||||
return {false, "请先填写以 http:// 或 https:// 开头的接口地址。", {}};
|
||||
|
||||
const HttpResponse response = PerformJsonRequest(ModelListEndpoint(endpoint), token, nullptr, kMaxModelListBytes);
|
||||
if (response.code != CURLE_OK || response.status < 200 || response.status >= 300)
|
||||
{
|
||||
return {false, "获取模型列表失败:" + ErrorDetail(response), {}};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const auto root = nlohmann::json::parse(response.body);
|
||||
std::vector<std::string> model_ids;
|
||||
if (root.contains("data") && root.at("data").is_array())
|
||||
{
|
||||
for (const auto &item : root.at("data"))
|
||||
{
|
||||
if (item.is_object() && item.contains("id") && item.at("id").is_string())
|
||||
{
|
||||
model_ids.push_back(item.at("id").get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (root.is_array())
|
||||
{
|
||||
for (const auto &item : root)
|
||||
{
|
||||
if (item.is_object() && item.contains("id") && item.at("id").is_string())
|
||||
{
|
||||
model_ids.push_back(item.at("id").get<std::string>());
|
||||
}
|
||||
else if (item.is_string())
|
||||
{
|
||||
model_ids.push_back(item.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (model_ids.empty())
|
||||
{
|
||||
return {false, "获取成功但未解析到可用模型。", {}};
|
||||
}
|
||||
|
||||
std::sort(model_ids.begin(), model_ids.end());
|
||||
model_ids.erase(std::unique(model_ids.begin(), model_ids.end()), model_ids.end());
|
||||
return {true, "获取成功", std::move(model_ids)};
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return {false, "解析服务返回的模型列表失败。", {}};
|
||||
}
|
||||
}
|
||||
} // namespace ApiCredentialTest
|
||||
Reference in new issue
Block a user