diff --git a/README.md b/README.md index f0dadeaaf..848cb54aa 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ ### AI inside + SQL inside -1. **AI Inside:** Run embedding, reranking, LLM inference and prompt management inside the database, supporting a complete document-in/data-out RAG workflow. +1. **AI Inside:** Run embedding, reranking, LLM inference and prompt management inside the database, supporting a complete document-in/data-out RAG workflow. Supported AI providers include OpenAI, DeepSeek, Aliyun (DashScope/OpenAI-compatible), SiliconFlow, Hunyuan, and [MiniMax](https://www.minimaxi.com). 2. **SQL Inside:** Powered by the proven OceanBase engine, delivering real-time writes and queries with full ACID compliance, and seamless MySQL ecosystem compatibility. diff --git a/README_CN.md b/README_CN.md index ebd12ba46..a466a11f1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -84,7 +84,7 @@ seekdb 提供了 Semantic Index 功能,只需写入文本数据,系统即可自动进行 Embedding 并生成向量索引,查询时仅需指定文本搜索条件即可进行语义搜索。该功能对用户屏蔽了向量嵌入和查询结果 Rerank 的复杂流程,显著简化 AI 应用开发对数据库的使用方式。 ### 无缝对接各类模型,内置 AI Function 实现库内实时推理 -seekdb 支持大语言模型和向量嵌入模型接入,通过 DBMS_AI_SERVICE 系统包实现模型注册和管理。内置 AI_COMPLETE、AI_PROMPT、AI_EMBED、AI_RERANK 等 AI Function,支持在标准 SQL 语法下进行数据嵌入和库内实时推理。 +seekdb 支持大语言模型和向量嵌入模型接入,通过 DBMS_AI_SERVICE 系统包实现模型注册和管理。内置 AI_COMPLETE、AI_PROMPT、AI_EMBED、AI_RERANK 等 AI Function,支持在标准 SQL 语法下进行数据嵌入和库内实时推理。支持的 AI 服务提供商包括 OpenAI、DeepSeek、阿里云(DashScope/OpenAI 兼容)、SiliconFlow、混元、以及 [MiniMax](https://www.minimaxi.com)。 ### 基于 JSON 的动态 Schema,支持文档元数据动态存储和高效访问 seekdb 支持 JSON 数据类型,具备动态 Schema 能力。支持 JSON 的部分更新以降低数据更新成本,提供 JSON 函数索引、多值索引来优化查询性能。实现半结构化编码降低存储成本。在 AI 应用中,JSON 可作为文档元信息的存储类型,并支持与全文、向量的混合搜索。 diff --git a/mittest/simple_server/test_ai_service.cpp b/mittest/simple_server/test_ai_service.cpp index 0e4d6352b..9e06bbc40 100644 --- a/mittest/simple_server/test_ai_service.cpp +++ b/mittest/simple_server/test_ai_service.cpp @@ -257,6 +257,93 @@ TEST_F(TestAiService, test_get_increment_ai_model_keys_reversely) } } +TEST_F(TestAiService, test_minimax_ai_model_endpoint) +{ + share::ObTenantSwitchGuard tenant_guard; + ASSERT_EQ(OB_SUCCESS, tenant_guard.switch_to(OB_SYS_TENANT_ID)); + ObTenantAiService *ai_service = MTL(ObTenantAiService*); + ObAiServiceGuard ai_service_guard; + const ObAiModelEndpointInfo *endpoint_info = nullptr; + + ObString endpoint_name = "minimax_endpoint"; + ObString ai_model_name = "minimax_ai_model"; + ObString url = "https://api.minimax.io/v1/chat/completions"; + ObString access_key = "minimax-test-key-1234567890"; + ObString provider = "minimax"; + ObString request_model_name = "MiniMax-M2.7"; + ObString parameters = ""; + ObString request_transform_fn = ""; + ObString response_transform_fn = ""; + common::ObArenaAllocator allocator; + ObSqlString sql; + + // 1. create MiniMax ai model endpoint + std::string json_str = R"({"url": ")"; + json_str += url.ptr(); + json_str += R"(", "access_key": ")"; + json_str += access_key.ptr(); + json_str += R"(", "ai_model_name": ")"; + json_str += ai_model_name.ptr(); + json_str += R"(", "provider": ")"; + json_str += provider.ptr(); + json_str += R"(", "request_model_name": ")"; + json_str += request_model_name.ptr(); + json_str += R"(", "parameters": ")"; + json_str += parameters.ptr(); + json_str += R"(", "request_transform_fn": ")"; + json_str += request_transform_fn.ptr(); + json_str += R"(", "response_transform_fn": ")"; + json_str += response_transform_fn.ptr(); + json_str += R"("})"; + sql.assign_fmt("call DBMS_AI_SERVICE.CREATE_AI_MODEL_ENDPOINT ('%s', '%s')", endpoint_name.ptr(), json_str.c_str()); + int64_t affected_rows = 0; + common::ObMySQLProxy &sql_proxy = get_curr_simple_server().get_sql_proxy(); + ASSERT_EQ(OB_SUCCESS, sql_proxy.write(sql.ptr(), affected_rows)); + + // 2. get MiniMax ai model endpoint by endpoint name + ASSERT_EQ(OB_SUCCESS, ai_service->get_ai_service_guard(ai_service_guard)); + ASSERT_EQ(OB_SUCCESS, ai_service_guard.get_ai_endpoint(endpoint_name, endpoint_info)); + ASSERT_TRUE(endpoint_info != nullptr); + check_ai_model_endpoint(*endpoint_info, allocator, endpoint_name, ai_model_name, url, access_key, + provider, request_model_name, parameters, request_transform_fn, response_transform_fn); + + // 3. get MiniMax ai model endpoint by ai model name + endpoint_info = nullptr; + ASSERT_EQ(OB_SUCCESS, ai_service_guard.get_ai_endpoint_by_ai_model_name(ai_model_name, endpoint_info)); + ASSERT_TRUE(endpoint_info != nullptr); + check_ai_model_endpoint(*endpoint_info, allocator, endpoint_name, ai_model_name, url, access_key, + provider, request_model_name, parameters, request_transform_fn, response_transform_fn); + + // 4. alter MiniMax endpoint to use embedding URL + url = "https://api.minimax.io/v1/embeddings"; + request_model_name = "embo-01"; + + json_str = R"({"url": ")"; + json_str += url.ptr(); + json_str += R"(", "request_model_name": ")"; + json_str += request_model_name.ptr(); + json_str += R"(", "request_transform_fn": ")"; + json_str += request_transform_fn.ptr(); + json_str += R"(", "response_transform_fn": ")"; + json_str += response_transform_fn.ptr(); + json_str += R"("})"; + + sql.assign_fmt("call DBMS_AI_SERVICE.ALTER_AI_MODEL_ENDPOINT ('%s', '%s')", endpoint_name.ptr(), json_str.c_str()); + ASSERT_EQ(OB_SUCCESS, sql_proxy.write(sql.ptr(), affected_rows)); + + // 5. verify altered endpoint + endpoint_info = nullptr; + ASSERT_EQ(OB_SUCCESS, ai_service_guard.get_ai_endpoint(endpoint_name, endpoint_info)); + ASSERT_TRUE(endpoint_info != nullptr); + check_ai_model_endpoint(*endpoint_info, allocator, endpoint_name, ai_model_name, url, access_key, + provider, request_model_name, parameters, request_transform_fn, response_transform_fn); + + // 6. drop MiniMax ai model endpoint + sql.assign_fmt("call DBMS_AI_SERVICE.DROP_AI_MODEL_ENDPOINT ('%s')", endpoint_name.ptr()); + ASSERT_EQ(OB_SUCCESS, sql_proxy.write(sql.ptr(), affected_rows)); + ASSERT_EQ(OB_AI_FUNC_ENDPOINT_NOT_FOUND, ai_service_guard.get_ai_endpoint(endpoint_name, endpoint_info)); +} + TEST_F(TestAiService, end) { RunCtx.time_sec_ = 0; diff --git a/src/share/ai_service/ob_ai_service_struct.cpp b/src/share/ai_service/ob_ai_service_struct.cpp index 2364ef271..f0cd33036 100644 --- a/src/share/ai_service/ob_ai_service_struct.cpp +++ b/src/share/ai_service/ob_ai_service_struct.cpp @@ -38,7 +38,8 @@ const char *VALID_PROVIDERS[] = { "DEEPSEEK", "SILICONFLOW", "HUNYUAN-OPENAI", - "OPENAI" + "OPENAI", + "MINIMAX" }; #define EXTRACT_JSON_ELEM_STR(json_key, member) \ diff --git a/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.cpp b/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.cpp index 7df880593..a4d4969a8 100644 --- a/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.cpp +++ b/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.cpp @@ -903,6 +903,102 @@ int ObSiliconflowUtils::ObSiliconflowRerank::parse_output(common::ObIAllocator & return ret; } +// MiniMax provider implementation +int ObMiniMaxUtils::get_header(common::ObIAllocator &allocator, + common::ObString &api_key, + common::ObArray &headers) +{ + // MiniMax uses the same Bearer token authentication as OpenAI + return ObOpenAIUtils::get_header(allocator, api_key, headers); +} + +int ObMiniMaxUtils::ObMiniMaxEmbed::get_header(common::ObIAllocator &allocator, + ObString &api_key, + common::ObArray &headers) +{ + int ret = OB_SUCCESS; + if (OB_FAIL(ObMiniMaxUtils::get_header(allocator, api_key, headers))) { + LOG_WARN("Failed to get header", K(ret)); + } + return ret; +} + +int ObMiniMaxUtils::ObMiniMaxEmbed::get_body(common::ObIAllocator &allocator, + common::ObString &model, + common::ObArray &contents, + common::ObJsonObject *config, + common::ObJsonObject *&body) +{ + int ret = OB_SUCCESS; + if (model.empty() || contents.empty()) { + ret = OB_INVALID_ARGUMENT; + LOG_WARN("Model name or contents is empty", K(ret)); + } else { + ObJsonObject *body_obj = nullptr; + ObJsonString *model_str = nullptr; + ObJsonArray *texts_array = nullptr; + ObJsonString *type_str = nullptr; + ObString type_val = ObString::make_string("db"); + if (OB_FAIL(ObAIFuncJsonUtils::get_json_object(allocator, body_obj))) { + LOG_WARN("Failed to get json object", K(ret)); + } else if (OB_FAIL(ObAIFuncJsonUtils::get_json_string(allocator, model, model_str))) { + LOG_WARN("Failed to get json string", K(ret)); + } else if (OB_FAIL(body_obj->add("model", model_str))) { + LOG_WARN("Failed to add model", K(ret)); + } else if (OB_FAIL(ObAIFuncJsonUtils::transform_array_to_json_array(allocator, contents, texts_array))) { + LOG_WARN("Failed to get json array", K(ret)); + } else if (OB_FAIL(body_obj->add("texts", texts_array))) { + LOG_WARN("Failed to add texts", K(ret)); + } else if (OB_FAIL(ObAIFuncJsonUtils::get_json_string(allocator, type_val, type_str))) { + LOG_WARN("Failed to get type string", K(ret)); + } else if (OB_FAIL(body_obj->add("type", type_str))) { + LOG_WARN("Failed to add type", K(ret)); + } else if (OB_FAIL(ObAIFuncJsonUtils::compact_json_object(allocator, config, body_obj))) { + LOG_WARN("Failed to compact json object", K(ret)); + } else { + body = body_obj; + } + } + return ret; +} + +int ObMiniMaxUtils::ObMiniMaxEmbed::parse_output(common::ObIAllocator &allocator, + common::ObJsonObject *http_response, + common::ObIJsonBase *&result) +{ + int ret = OB_SUCCESS; + if (OB_ISNULL(http_response)) { + ret = OB_INVALID_ARGUMENT; + LOG_WARN("http_response is null", K(ret)); + } else { + ObJsonArray *result_array = nullptr; + ObJsonNode *vectors_node = nullptr; + if (OB_FAIL(ObAIFuncJsonUtils::get_json_array(allocator, result_array))) { + LOG_WARN("Failed to get json array", K(ret)); + } else if (OB_ISNULL(vectors_node = http_response->get_value("vectors"))) { + ret = OB_INVALID_DATA; + LOG_WARN("Failed to get vectors from MiniMax response", K(ret)); + } else { + // MiniMax returns {"vectors": [[0.1, 0.2, ...], [0.3, 0.4, ...]], ...} + // Each element in vectors is already an embedding array + ObJsonArray *vectors_array = static_cast(vectors_node); + for (int64_t i = 0; OB_SUCC(ret) && i < vectors_array->element_count(); i++) { + ObJsonNode *embedding = vectors_array->get_value(i); + if (OB_ISNULL(embedding)) { + ret = OB_INVALID_DATA; + LOG_WARN("Failed to get embedding vector", K(ret), K(i)); + } else if (OB_FAIL(result_array->append(embedding))) { + LOG_WARN("Failed to append embedding", K(ret)); + } + } + if (OB_SUCC(ret)) { + result = result_array; + } + } + } + return ret; +} + int ObAIFuncUtils::get_header(ObIAllocator &allocator, const ObAIFuncExprInfo &info, @@ -1241,7 +1337,8 @@ int ObAIFuncUtils::get_complete_provider(ObIAllocator &allocator, const ObString || ob_provider_check(provider, ObAIFuncProviderUtils::ALIYUN) || ob_provider_check(provider, ObAIFuncProviderUtils::DEEPSEEK) || ob_provider_check(provider, ObAIFuncProviderUtils::SILICONFLOW) - || ob_provider_check(provider, ObAIFuncProviderUtils::HUNYUAN)) { + || ob_provider_check(provider, ObAIFuncProviderUtils::HUNYUAN) + || ob_provider_check(provider, ObAIFuncProviderUtils::MINIMAX)) { complete_provider = OB_NEWx(ObOpenAIUtils::ObOpenAIComplete, &allocator); } else if (ob_provider_check(provider, ObAIFuncProviderUtils::DASHSCOPE)) { complete_provider = OB_NEWx(ObDashscopeUtils::ObDashscopeComplete, &allocator); @@ -1271,6 +1368,8 @@ int ObAIFuncUtils::get_embed_provider(ObIAllocator &allocator, const ObString &p embed_provider = OB_NEWx(ObOpenAIUtils::ObOpenAIEmbed, &allocator); } else if (ob_provider_check(provider, ObAIFuncProviderUtils::DASHSCOPE)) { embed_provider = OB_NEWx(ObDashscopeUtils::ObDashscopeEmbed, &allocator); + } else if (ob_provider_check(provider, ObAIFuncProviderUtils::MINIMAX)) { + embed_provider = OB_NEWx(ObMiniMaxUtils::ObMiniMaxEmbed, &allocator); } else { ret = OB_NOT_SUPPORTED; LOG_WARN("this provider current not support", K(ret)); diff --git a/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.h b/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.h index 5701ef8ae..0a7b50450 100644 --- a/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.h +++ b/src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.h @@ -230,6 +230,35 @@ class ObSiliconflowUtils private: DISALLOW_COPY_AND_ASSIGN(ObSiliconflowUtils); }; +class ObMiniMaxUtils +{ +public: + class ObMiniMaxEmbed : public ObAIFuncIEmbed + { + public: + ObMiniMaxEmbed() {} + virtual ~ObMiniMaxEmbed() {} + virtual int get_header(common::ObIAllocator &allocator, + common::ObString &api_key, + common::ObArray &headers) override; + virtual int get_body(common::ObIAllocator &allocator, + common::ObString &model, + common::ObArray &contents, + common::ObJsonObject *config, + common::ObJsonObject *&body) override; + virtual int parse_output(common::ObIAllocator &allocator, + common::ObJsonObject *http_response, + common::ObIJsonBase *&result) override; + private: + DISALLOW_COPY_AND_ASSIGN(ObMiniMaxEmbed); + }; + static int get_header(common::ObIAllocator &allocator, + common::ObString &api_key, + common::ObArray &headers); +private: + DISALLOW_COPY_AND_ASSIGN(ObMiniMaxUtils); +}; + class ObAIFuncJsonUtils { public: @@ -337,6 +366,7 @@ class ObAIFuncProviderUtils static constexpr char SILICONFLOW[20] = "SILICONFLOW"; static constexpr char HUNYUAN[20] = "HUNYUAN-OPENAI"; static constexpr char DEEPSEEK[20] = "DEEPSEEK"; + static constexpr char MINIMAX[20] = "MINIMAX"; private: DISALLOW_COPY_AND_ASSIGN(ObAIFuncProviderUtils); diff --git a/unittest/sql/engine/expr/CMakeLists.txt b/unittest/sql/engine/expr/CMakeLists.txt index f91fa0e4c..1a69c0bf5 100644 --- a/unittest/sql/engine/expr/CMakeLists.txt +++ b/unittest/sql/engine/expr/CMakeLists.txt @@ -10,6 +10,7 @@ sql_unittest(test_expr_relation_map) ob_unittest(test_ob_openai_utils) ob_unittest(test_ob_dashscope_utils) ob_unittest(test_ob_siliconflow_utils) +ob_unittest(test_ob_minimax_utils) ob_unittest(ob_expr_ai_prompt_test) # engine_expr_test_lrpad_SOURCES=engine/expr/ob_expr_lrpad_test.cpp diff --git a/unittest/sql/engine/expr/test_ob_minimax_utils.cpp b/unittest/sql/engine/expr/test_ob_minimax_utils.cpp new file mode 100644 index 000000000..df85bb2b4 --- /dev/null +++ b/unittest/sql/engine/expr/test_ob_minimax_utils.cpp @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2025 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "ob_expr_test_utils.h" +#include "sql/engine/expr/ob_expr_ai/ob_ai_func_utils.h" + +using namespace oceanbase::common; +using namespace oceanbase::sql; + +class ObMiniMaxUtilsTest: public ::testing::Test +{ +public: + ObMiniMaxUtilsTest(); + virtual ~ObMiniMaxUtilsTest(); + virtual void SetUp(); + virtual void TearDown(); +private: + // disallow copy + ObMiniMaxUtilsTest(const ObMiniMaxUtilsTest &other); + ObMiniMaxUtilsTest& operator=(const ObMiniMaxUtilsTest &other); +protected: + // data members +}; + +ObMiniMaxUtilsTest::ObMiniMaxUtilsTest() +{ +} + +ObMiniMaxUtilsTest::~ObMiniMaxUtilsTest() +{ +} + +void ObMiniMaxUtilsTest::SetUp() +{ +} + +void ObMiniMaxUtilsTest::TearDown() +{ +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_get_header) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString api_key("minimax-test-key-1234567890"); + ObString authorization("Authorization: Bearer minimax-test-key-1234567890"); + ObString content_type("Content-Type: application/json"); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObArray headers; + ASSERT_EQ(OB_SUCCESS, embedding.get_header(allocator, api_key, headers)); + ASSERT_EQ(2, headers.count()); + ASSERT_EQ(authorization, headers[0]); + ASSERT_EQ(content_type, headers[1]); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_get_body) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString model("embo-01"); + ObString input("oceanbase seekdb is an AI-native search database"); + ObArray input_array; + input_array.push_back(input); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObJsonObject *body = nullptr; + ObJsonObject *config = nullptr; + ASSERT_EQ(OB_SUCCESS, embedding.get_body(allocator, model, input_array, config, body)); + + // Check model field + ObJsonNode *model_node = body->get_value("model"); + ObStringBuffer model_buf(&allocator); + model_node->print(model_buf, 0); + ASSERT_EQ(model, model_buf.string()); + + // Check texts field (MiniMax uses "texts" instead of "input") + ObJsonNode *texts_node = body->get_value("texts"); + ASSERT_TRUE(texts_node != nullptr); + ObJsonArray *texts_array_node = static_cast(texts_node); + ASSERT_EQ(1, texts_array_node->element_count()); + ObJsonNode *text_node = texts_array_node->get_value(0); + ObStringBuffer text_buf(&allocator); + text_node->print(text_buf, 0); + ASSERT_EQ(input, text_buf.string()); + + // Check type field (MiniMax requires "type" field) + ObJsonNode *type_node = body->get_value("type"); + ASSERT_TRUE(type_node != nullptr); + ObStringBuffer type_buf(&allocator); + type_node->print(type_buf, 0); + ASSERT_EQ(ObString("db"), type_buf.string()); + + // Verify "input" field does NOT exist (MiniMax uses "texts") + ObJsonNode *input_node = body->get_value("input"); + ASSERT_TRUE(input_node == nullptr); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_get_body_multiple_texts) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString model("embo-01"); + ObString text1("vector search is powerful"); + ObString text2("hybrid search combines vector and text"); + ObArray input_array; + input_array.push_back(text1); + input_array.push_back(text2); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObJsonObject *body = nullptr; + ObJsonObject *config = nullptr; + ASSERT_EQ(OB_SUCCESS, embedding.get_body(allocator, model, input_array, config, body)); + + // Check texts array has 2 elements + ObJsonNode *texts_node = body->get_value("texts"); + ASSERT_TRUE(texts_node != nullptr); + ObJsonArray *texts_array = static_cast(texts_node); + ASSERT_EQ(2, texts_array->element_count()); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_get_body_empty_model) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString model(""); + ObString input("test"); + ObArray input_array; + input_array.push_back(input); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObJsonObject *body = nullptr; + ObJsonObject *config = nullptr; + ASSERT_EQ(OB_INVALID_ARGUMENT, embedding.get_body(allocator, model, input_array, config, body)); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_get_body_empty_contents) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString model("embo-01"); + ObArray input_array; + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObJsonObject *body = nullptr; + ObJsonObject *config = nullptr; + ASSERT_EQ(OB_INVALID_ARGUMENT, embedding.get_body(allocator, model, input_array, config, body)); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_parse_output) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString content("[0.0023064255, -0.009327292, -0.0028842222]"); + // MiniMax embedding response uses "vectors" instead of "data" + ObString response( + "{" + "\"vectors\": [" + "[0.0023064255, -0.009327292, -0.0028842222]" + "]," + "\"total_tokens\": 10," + "\"base_resp\": {" + "\"status_code\": 0," + "\"status_msg\": \"success\"" + "}" + "}" + ); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObIJsonBase *j_base = nullptr; + ASSERT_EQ(OB_SUCCESS, ObJsonBaseFactory::get_json_base(&allocator, response, ObJsonInType::JSON_TREE, ObJsonInType::JSON_TREE, j_base)); + ObJsonObject *http_response = static_cast(j_base); + ObIJsonBase *result = nullptr; + ASSERT_EQ(OB_SUCCESS, embedding.parse_output(allocator, http_response, result)); + + ObJsonArray *embeddings_array = static_cast(result); + ASSERT_EQ(1, embeddings_array->element_count()); + + ObJsonNode *embedding_node = embeddings_array->get_value(0); + ObJsonArray *embedding_array = static_cast(embedding_node); + ObStringBuffer embedding_buf(&allocator); + embedding_array->print(embedding_buf, 0); + ASSERT_EQ(content, embedding_buf.string()); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_parse_output_multiple_vectors) +{ + ObArenaAllocator allocator(ObModIds::TEST); + // MiniMax returns multiple vectors for batch requests + ObString response( + "{" + "\"vectors\": [" + "[0.1, 0.2, 0.3]," + "[0.4, 0.5, 0.6]" + "]," + "\"total_tokens\": 20," + "\"base_resp\": {" + "\"status_code\": 0," + "\"status_msg\": \"success\"" + "}" + "}" + ); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObIJsonBase *j_base = nullptr; + ASSERT_EQ(OB_SUCCESS, ObJsonBaseFactory::get_json_base(&allocator, response, ObJsonInType::JSON_TREE, ObJsonInType::JSON_TREE, j_base)); + ObJsonObject *http_response = static_cast(j_base); + ObIJsonBase *result = nullptr; + ASSERT_EQ(OB_SUCCESS, embedding.parse_output(allocator, http_response, result)); + + ObJsonArray *embeddings_array = static_cast(result); + ASSERT_EQ(2, embeddings_array->element_count()); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_parse_output_empty) +{ + ObArenaAllocator allocator(ObModIds::TEST); + // Response without "vectors" field should fail + ObString response( + "{" + "\"total_tokens\": 10," + "\"base_resp\": {" + "\"status_code\": 0," + "\"status_msg\": \"success\"" + "}" + "}" + ); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObIJsonBase *j_base = nullptr; + ASSERT_EQ(OB_SUCCESS, ObJsonBaseFactory::get_json_base(&allocator, response, ObJsonInType::JSON_TREE, ObJsonInType::JSON_TREE, j_base)); + ObJsonObject *http_response = static_cast(j_base); + ObIJsonBase *result = nullptr; + ASSERT_EQ(OB_INVALID_DATA, embedding.parse_output(allocator, http_response, result)); +} + +TEST_F(ObMiniMaxUtilsTest, test_embedding_parse_output_null_response) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObMiniMaxUtils::ObMiniMaxEmbed embedding; + ObIJsonBase *result = nullptr; + ASSERT_EQ(OB_INVALID_ARGUMENT, embedding.parse_output(allocator, nullptr, result)); +} + +TEST_F(ObMiniMaxUtilsTest, test_provider_constant) +{ + // Verify that MINIMAX provider constant is correctly defined + ObString minimax_provider(ObAIFuncProviderUtils::MINIMAX); + ASSERT_EQ(ObString("MINIMAX"), minimax_provider); +} + +TEST_F(ObMiniMaxUtilsTest, test_provider_routing_complete) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString minimax_provider("MINIMAX"); + ObAIFuncIComplete *complete_provider = nullptr; + // MiniMax completion should route to OpenAI-compatible handler + ASSERT_EQ(OB_SUCCESS, ObAIFuncUtils::get_complete_provider(allocator, minimax_provider, complete_provider)); + ASSERT_TRUE(complete_provider != nullptr); +} + +TEST_F(ObMiniMaxUtilsTest, test_provider_routing_embed) +{ + ObArenaAllocator allocator(ObModIds::TEST); + ObString minimax_provider("MINIMAX"); + ObAIFuncIEmbed *embed_provider = nullptr; + // MiniMax embedding should route to MiniMax-specific handler + ASSERT_EQ(OB_SUCCESS, ObAIFuncUtils::get_embed_provider(allocator, minimax_provider, embed_provider)); + ASSERT_TRUE(embed_provider != nullptr); +} + + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc,argv); + return RUN_ALL_TESTS(); +}