diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 708275fa..ccde4a20 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +Converter 定义精简(不涉及 Operator ABI、Pipeline JSON 与 `.conf`):删除没有运行时作用的 +`schema_version`、`external_type`、输出 `cardinality` 与 `capacity_policy`,以及槽位的 `value_type` 与 +`capacity_fields`。输出槽容量字段只由 ValueType 决定,`ExternalInputSlot` / `ExternalOutputSlot` +只接受槽名。Catalog 的 Converter 不再导出上述元数据,`external_type` 改由槽位类型推导。 +`schema_id` 保留为外部协议 ID:载体相同的 Converter 可能解析语义不同,同业务 binding 的一致性检查 +继续比较它。 + 开发工具修复:Markdown 链接检查支持单引号与圆括号标题、带空格的尖括号目标及平衡或转义的 目标圆括号;生产版 `alg_pipeline_tool edit` 根据 `validation.diagnostics` 提示构建变体与测试工具。 交付脚本要求远端 PR head 与已验证提交一致,以该 SHA 复核历史,并通过 `--match-head-commit` @@ -17,8 +24,8 @@ - Model 继承 `ModelIdentity`、Backend Provider 继承 `BackendIdentity`, 身份只声明一次,Definition 从 `MakeModelDefinition` / `MakeBackendDefinition` 开始; 配置读取使用 `ConfigValueOrDefault`,默认值只写在 `config_fields`。 -- 转换器 `external_type` 留空时由外部槽类型推导;`InputPortBindings` / `OutputPortBindings` - 共用 `PortBindings<方向>`;`ResolvedInputLimits` 更名为 `InputLimits` 且不再出现在部署配置中。 +- `InputPortBindings` / `OutputPortBindings` 共用 `PortBindings<方向>`;`ResolvedInputLimits` + 更名为 `InputLimits` 且不再出现在部署配置中。 - 删除 `include/adapter/biz_results.h`、`ModelManager::RegisterModel`(改用 `RegisterBatch`)和 `RuntimeOptions` 中只写不读的 `biz_type`、`depth_num`、`biz_name`;`NodeBase` 的类写法端口辅助 函数移到 `dev_support` 的 `LegacyNodeBase`。 diff --git a/doc/dev_guide/business_onboarding.md b/doc/dev_guide/business_onboarding.md index 6f69305a..74c245a1 100644 --- a/doc/dev_guide/business_onboarding.md +++ b/doc/dev_guide/business_onboarding.md @@ -152,7 +152,7 @@ Operator 的宿主输入校验会拒绝 `CompanyString` 中的原始嵌入 NUL 外部必需槽的常见写法是 `ExternalInputSlot(slot)` 和 `ExternalOutputSlot(slot)`,类型由 traits 推导。 -输出容量字段由已注册 ValueType 的字符串容量字段决定;显式列出时必须与该 ValueType 的字段集合一致。 +输出容量字段由已注册 ValueType 的字符串容量字段决定,不在槽位上声明。 槽名、类型后缀和外部 key 后缀不全相同、可选槽或特殊布局时,使用完整的 `ExternalSlotDefinition`, 各字段含义和示例见[选择参数](operator_output_allocation.md#选择参数)。 @@ -206,8 +206,8 @@ ValueType。宿主类型的转换都留在接入适配层,Node、Model 和 Bac 已有契约的新方案直接沿用对应 Demo 和数据集格式,只准备 Pipeline 与指向它的 `.conf`。 新契约先检查已有 Demo 运行代码是否支持所需载体、槽位及数据集格式,可满足时复用这些代码。 Pipeline 只选择 `io_binding`,Demo 调用 `ResolveOperatorConfigBiz` 解析业务身份并选择 runner。 -Profile 不填写业务名。SDK 预检与注册审计共用同业务 binding 的外部协议、载体及槽位一致性检查。Demo 按该契约准备载体, -不能仅因宿主类型相同就复用另一业务契约;同一业务也不能登记不兼容的载体。 +Profile 不填写业务名。SDK 预检与注册审计共用同业务 binding 的外部协议(Converter 的 `schema_id`)、载体及槽位一致性检查。Demo 按该契约准备载体, +不能仅因宿主类型相同就复用另一业务契约;同一业务也不能登记载体或协议不兼容的 binding。 这里的数据转换仅指载体构造和结果展示,外部协议的解包、 字段选择与响应组装仍在转换器;不得把原始业务请求预先拆成内部节点输入。 新绑定需要接入统一 Demo 时,按 `keyword_match_demo.cpp` 补齐以下部分: diff --git a/doc/dev_guide/operator_output_allocation.md b/doc/dev_guide/operator_output_allocation.md index 5a602754..a9199d58 100644 --- a/doc/dev_guide/operator_output_allocation.md +++ b/doc/dev_guide/operator_output_allocation.md @@ -52,12 +52,11 @@ JSON 读取器将选中值通过 `dump()` 转为拥有自身存储的 `std::stri 转换器声明槽位时,`ExternalInputSlot(slot)` / `ExternalOutputSlot(slot)` 令 `type_suffix = slot_name`,`key_suffix` 留空并回退到 `type_suffix`,只适用于必需槽且三个名称相同的 -常见约定;输入工厂的第二参数是 `value_type`,不能用来覆盖后缀。其他情况使用完整的 -`ExternalSlotDefinition`。例如逻辑槽名为 `result`、已注册类型后缀为 `entity_out`、外部 key 为 -`sdk.answer` 时,分别设置 `slot_name = "result"`、`type_suffix = "entity_out"`、`key_suffix = "answer"`。 -`schema_version`、输出 `cardinality`、`capacity_policy` 使用 Definition 的默认值时无需赋值; -`external_type` 留空时由外部槽类型按声明顺序拼接(如 `CompanyFrame,CompanyString`)。 -规则不同时显式填写。 +常见约定。其他情况使用完整的 `ExternalSlotDefinition`。例如逻辑槽名为 `result`、已注册类型后缀为 +`entity_out`、外部 key 为 `sdk.answer` 时,分别设置 `slot_name = "result"`、 +`type_suffix = "entity_out"`、`key_suffix = "answer"`。 +输出槽的容量字段由 `type_suffix` 对应的 ValueType 决定,不在槽位上重复声明;Catalog 中的 +`external_type` 由外部槽类型按声明顺序拼接(如 `CompanyFrame,CompanyString`)。 以下配置来自参与编译的 [测试接入](../../tests/integration/operator/test_operator_api.cpp)和 diff --git a/doc/dev_guide/source_layout.md b/doc/dev_guide/source_layout.md index deedca7d..0686be04 100644 --- a/doc/dev_guide/source_layout.md +++ b/doc/dev_guide/source_layout.md @@ -88,8 +88,8 @@ src/adapter/ 各业务接入绑定在 `src/adapter/biz/` 中声明 `IoBindingDefinition`,通过 `REGISTER_IO_BINDING` 注册。 端口 Definition、回调的 `bindings.Key(port)` 和 `BindIoPort` 映射共用 typed 声明; 非同名映射显式传入逻辑端口与实际 key。常见必需槽可用 `ExternalInputSlot` / -`ExternalOutputSlot` 推导类型和默认同名后缀,输出容量字段由已注册 ValueType 推导; -显式列出时字段集合必须一致,特殊布局仍使用完整定义。 +`ExternalOutputSlot` 推导类型和默认同名后缀,输出容量字段由已注册 ValueType 决定; +特殊布局仍使用完整定义。 业务专属实现可按修改关联同文件组织,共享 converter 保留独立引用;不要求为每个业务创建聚合宏或新注册表。 宿主值类型与命名输出分配方案通过 `adapter/operator_value_type.h` 登记;实现只管理 单份结构及嵌套存储,队列、租约和初始化审计归通用机制所有。常见类型直接使用 diff --git a/include/adapter/converter_authoring.h b/include/adapter/converter_authoring.h index 9e70949b..80095ac5 100644 --- a/include/adapter/converter_authoring.h +++ b/include/adapter/converter_authoring.h @@ -33,29 +33,16 @@ inline std::pair BindIoPort( // 常用约定:必填槽位,type_suffix = slot_name,key_suffix 为空时回退到 // type_suffix。其他后缀或可选槽位请使用 ExternalSlotDefinition 的显式字段。 template -inline ExternalSlotDefinition ExternalInputSlot( - std::string slot_name, - std::string value_type = ExternalTypeTraits::TypeName()) { - return {slot_name, - ExternalTypeTraits::TypeName(), - PortDirection::kInput, - true, - std::move(value_type), - slot_name}; +inline ExternalSlotDefinition ExternalInputSlot(std::string slot_name) { + return {slot_name, ExternalTypeTraits::TypeName(), PortDirection::kInput, + true, slot_name}; } -// 省略容量字段时由已注册的 ValueType 推导; -// 显式列表必须与该 ValueType 一致。 +// 容量字段由已注册的 ValueType 决定。 template -inline ExternalSlotDefinition ExternalOutputSlot( - std::string slot_name, std::vector capacity_fields = {}) { - return {slot_name, - ExternalTypeTraits::TypeName(), - PortDirection::kOutput, - true, - ExternalTypeTraits::TypeName(), - slot_name, - std::move(capacity_fields)}; +inline ExternalSlotDefinition ExternalOutputSlot(std::string slot_name) { + return {slot_name, ExternalTypeTraits::TypeName(), PortDirection::kOutput, + true, slot_name}; } // Operator 在解码前已检查载体和有效批大小上限,并通过 options 传入; diff --git a/include/adapter/io_binding_registry.h b/include/adapter/io_binding_registry.h index 61223b00..8e67d86d 100644 --- a/include/adapter/io_binding_registry.h +++ b/include/adapter/io_binding_registry.h @@ -17,8 +17,8 @@ std::unordered_map EffectivePortMapping( const std::unordered_map& declared, const std::vector& logical_ports); -// 未声明 capacity_fields 的输出槽位,按字典序继承其已注册 ValueType 的 -// 字符串容量字段。输入槽位和未知 ValueType 原样返回声明列表。 +// 输出槽位按字典序返回其已注册 ValueType 的字符串容量字段; +// 输入槽位和未知 ValueType 返回空列表。 std::vector EffectiveCapacityFields( const ExternalSlotDefinition& slot); diff --git a/include/adapter/io_converter.h b/include/adapter/io_converter.h index bee71055..a36be045 100644 --- a/include/adapter/io_converter.h +++ b/include/adapter/io_converter.h @@ -201,26 +201,20 @@ struct ExternalSlotDefinition { std::string type_id; PortDirection direction = PortDirection::kInput; bool required = true; - std::string value_type; std::string type_suffix; // Operator ValueType 规范后缀 (如 "plain_text", // "entity_out") - std::vector capacity_fields; std::string key_suffix; // 外部 map key 后缀 (为空时使用 type_suffix) ExternalSlotDefinition() = default; ExternalSlotDefinition(std::string slot_name, std::string type_id, PortDirection direction = PortDirection::kInput, - bool required = true, std::string value_type = "", - std::string type_suffix = "", - std::vector capacity_fields = {}, + bool required = true, std::string type_suffix = "", std::string key_suffix = "") : slot_name(std::move(slot_name)), type_id(std::move(type_id)), direction(direction), required(required), - value_type(std::move(value_type)), type_suffix(std::move(type_suffix)), - capacity_fields(std::move(capacity_fields)), key_suffix(std::move(key_suffix)) {} const std::string& KeySuffix() const { @@ -228,6 +222,17 @@ struct ExternalSlotDefinition { } }; +// 外部载体类型标签:按声明顺序拼接槽位类型,如 "CompanyFrame,CompanyString"。 +inline std::string ExternalType( + const std::vector& slots) { + std::string joined; + for (const auto& slot : slots) { + if (!joined.empty()) joined += ","; + joined += slot.type_id; + } + return joined; +} + // 统一输入/输出转换回调函数指针类型 using DecodeInputFn = int (*)(const ExternalInputBatchView& source, const InputDecodeOptions& options, @@ -246,9 +251,8 @@ using EncodeOutputFn = int (*)(AlgContext* context, struct InputConverterDefinition { std::string converter_id; + // 外部载荷协议 ID。载体相同时,同一 biz 的多个 Binding 依靠它区分解析语义。 std::string schema_id; - int schema_version = 1; - std::string external_type; std::vector external_slots; std::vector logical_ports; // 发布的内部逻辑输出端口 // 可选的 Converter 专属上限;0 表示不设上限。 @@ -263,15 +267,12 @@ struct InputConverterDefinition { struct OutputConverterDefinition { std::string converter_id; + // 外部响应协议 ID,规则同 InputConverterDefinition::schema_id。 std::string schema_id; - int schema_version = 1; - std::string external_type; std::vector logical_ports; // 消费的内部逻辑输入端口 std::vector external_slots; - std::string cardinality = "1:1"; // 可选的 Converter 专属上限;0 表示不设上限。 size_t max_batch_size = 0; - std::string capacity_policy = "reject_overflow"; EncodeOutputFn encode_fn = nullptr; }; diff --git a/src/adapter/input/text_input.cpp b/src/adapter/input/text_input.cpp index d0c9b3f9..3a1b7140 100644 --- a/src/adapter/input/text_input.cpp +++ b/src/adapter/input/text_input.cpp @@ -57,7 +57,7 @@ InputConverterDefinition MakeOperatorEntityInputConverter() { def.schema_id = "text.plain.request"; def.external_slots = { - ExternalInputSlot(kEntitySlot, kEntitySlot)}; + ExternalInputSlot(kEntitySlot)}; def.logical_ports = {OutputPort(kInputSentences)}; def.decode_fn = &DecodeOperatorEntityInput; return def; @@ -69,8 +69,8 @@ InputConverterDefinition MakeOperatorKeywordInputConverter() { def.schema_id = "text.plain.request"; - def.external_slots = {ExternalInputSlot( - kKeywordSlot, kKeywordSlot)}; + def.external_slots = { + ExternalInputSlot(kKeywordSlot)}; def.logical_ports = {OutputPort(kInputSentences)}; def.decode_fn = &DecodeOperatorKeywordInput; return def; diff --git a/src/adapter/input/translate_json_input.cpp b/src/adapter/input/translate_json_input.cpp index a074a3cd..9675ee40 100644 --- a/src/adapter/input/translate_json_input.cpp +++ b/src/adapter/input/translate_json_input.cpp @@ -63,7 +63,7 @@ InputConverterDefinition MakeOperatorTranslateJsonInputConverter() { def.schema_id = "translate.json.request"; def.external_slots = { - ExternalInputSlot(kInputSlot, kInputSlot)}; + ExternalInputSlot(kInputSlot)}; def.logical_ports = {OutputPort(kInputSentences)}; def.decode_fn = &DecodeOperatorTranslateJson; return def; diff --git a/src/adapter/io_binding_registry.cpp b/src/adapter/io_binding_registry.cpp index 996fe628..fef07cd8 100644 --- a/src/adapter/io_binding_registry.cpp +++ b/src/adapter/io_binding_registry.cpp @@ -1,7 +1,6 @@ #include "adapter/io_binding_registry.h" #include -#include #include #include "adapter/io_converter_registry.h" @@ -11,19 +10,16 @@ namespace llm_edgeflow { namespace { +// 容量字段由 type_suffix 对应的 ValueType 决定,无需单独比较。 bool SameExternalSlots(const std::vector& left, const std::vector& right) { const auto signature = [](const auto& slots) { using Slot = - std::tuple>; + std::tuple; std::vector result; for (const auto& slot : slots) { - auto capacities = EffectiveCapacityFields(slot); - std::sort(capacities.begin(), capacities.end()); result.emplace_back(slot.KeySuffix(), slot.type_id, slot.type_suffix, - slot.direction, slot.required, slot.value_type, - std::move(capacities)); + slot.direction, slot.required); } std::sort(result.begin(), result.end()); return result; @@ -31,11 +27,10 @@ bool SameExternalSlots(const std::vector& left, return signature(left) == signature(right); } +// 载体与槽位相同的 Converter 仍可能按不同协议解析,须同时比较 schema_id。 template bool SameExternalContract(const Converter& left, const Converter& right) { return left.schema_id == right.schema_id && - left.schema_version == right.schema_version && - left.external_type == right.external_type && SameExternalSlots(left.external_slots, right.external_slots); } @@ -60,11 +55,8 @@ bool CheckBizContract(std::vector bindings, "' references an unregistered converter"; return false; } - if (first_input && - (!SameExternalContract(*first_input, *input) || - !SameExternalContract(*first_output, *output) || - first_output->cardinality != output->cardinality || - first_output->capacity_policy != output->capacity_policy)) { + if (first_input && (!SameExternalContract(*first_input, *input) || + !SameExternalContract(*first_output, *output))) { if (error) *error = "Bindings '" + first_binding + "' and '" + binding.binding_id + "' for biz_name '" + biz_name + @@ -82,11 +74,10 @@ bool CheckBizContract(std::vector bindings, std::vector EffectiveCapacityFields( const ExternalSlotDefinition& slot) { - if (slot.direction != PortDirection::kOutput || !slot.capacity_fields.empty()) - return slot.capacity_fields; + if (slot.direction != PortDirection::kOutput) return {}; const auto* binding = OperatorValueTypeRegistry::Instance().GetOutputBinding( slot.type_suffix, ""); - if (!binding) return slot.capacity_fields; + if (!binding) return {}; std::vector fields; for (const auto& [name, config] : binding->output_layout.string_capacity_fields) @@ -393,19 +384,6 @@ bool IoBindingRegistry::Audit(std::vector* out_errors) const { errors.push_back( "Binding '" + binding_id + "' output slot '" + slot.slot_name + "' uses unregistered ValueType suffix: " + slot.type_suffix); - } else if (!slot.capacity_fields.empty()) { - std::set expected; - for (const auto& [name, config] : - val_binding->output_layout.string_capacity_fields) - expected.insert(name); - const std::set declared(slot.capacity_fields.begin(), - slot.capacity_fields.end()); - if (declared != expected) { - errors.push_back("Binding '" + binding_id + "' output slot '" + - slot.slot_name + - "' capacity_fields do not match ValueType '" + - slot.type_suffix + "'"); - } } } } diff --git a/src/adapter/io_catalog.cpp b/src/adapter/io_catalog.cpp index e8470a66..200c263f 100644 --- a/src/adapter/io_catalog.cpp +++ b/src/adapter/io_catalog.cpp @@ -18,7 +18,6 @@ nlohmann::json SlotJson(const ExternalSlotDefinition& slot) { {"key_suffix", slot.KeySuffix()}, {"direction", slot.direction == PortDirection::kInput ? "input" : "output"}, - {"value_type", slot.value_type}, {"required", slot.required}, {"capacity_fields", EffectiveCapacityFields(slot)}}; } @@ -33,8 +32,7 @@ nlohmann::json InputConverterToJson(const InputConverterDefinition& conv) { return {{"converter_id", conv.converter_id}, {"transport", "operator"}, {"schema_id", conv.schema_id}, - {"schema_version", conv.schema_version}, - {"external_type", conv.external_type}, + {"external_type", ExternalType(conv.external_slots)}, {"max_batch_size", conv.max_batch_size}, {"external_slots", std::move(slots)}, {"logical_ports", std::move(ports)}}; @@ -50,11 +48,8 @@ nlohmann::json OutputConverterToJson(const OutputConverterDefinition& conv) { return {{"converter_id", conv.converter_id}, {"transport", "operator"}, {"schema_id", conv.schema_id}, - {"schema_version", conv.schema_version}, - {"external_type", conv.external_type}, + {"external_type", ExternalType(conv.external_slots)}, {"max_batch_size", conv.max_batch_size}, - {"cardinality", conv.cardinality}, - {"capacity_policy", conv.capacity_policy}, {"external_slots", std::move(slots)}, {"logical_ports", std::move(ports)}}; } diff --git a/src/adapter/io_converter_registry.cpp b/src/adapter/io_converter_registry.cpp index 391746af..3d30af84 100644 --- a/src/adapter/io_converter_registry.cpp +++ b/src/adapter/io_converter_registry.cpp @@ -2,21 +2,6 @@ namespace llm_edgeflow { -namespace { - -// 默认协议载体标签:按声明顺序列出外部槽位类型, -// 如 "CompanyFrame,CompanyString"。 -std::string JoinedSlotTypes(const std::vector& slots) { - std::string joined; - for (const auto& slot : slots) { - if (!joined.empty()) joined += ","; - joined += slot.type_id; - } - return joined; -} - -} // namespace - IoConverterRegistry& IoConverterRegistry::Instance() { static IoConverterRegistry instance; return instance; @@ -40,12 +25,6 @@ bool IoConverterRegistry::RegisterInputConverter( def.converter_id); return false; } - if (def.schema_version < 1) { - conflicts_.Record("Invalid schema_version (" + - std::to_string(def.schema_version) + - ") in InputConverterDefinition for: " + def.converter_id); - return false; - } if (def.external_slots.empty()) { conflicts_.Record("Empty external_slots in InputConverterDefinition for: " + def.converter_id); @@ -87,11 +66,7 @@ bool IoConverterRegistry::RegisterInputConverter( return false; } - auto& stored = input_converters_[def.converter_id]; - stored = def; - if (stored.external_type.empty()) { - stored.external_type = JoinedSlotTypes(stored.external_slots); - } + input_converters_.emplace(def.converter_id, def); return true; } @@ -113,12 +88,6 @@ bool IoConverterRegistry::RegisterOutputConverter( def.converter_id); return false; } - if (def.schema_version < 1) { - conflicts_.Record( - "Invalid schema_version (" + std::to_string(def.schema_version) + - ") in OutputConverterDefinition for: " + def.converter_id); - return false; - } if (def.external_slots.empty()) { conflicts_.Record( "Empty external_slots in OutputConverterDefinition for: " + @@ -161,11 +130,7 @@ bool IoConverterRegistry::RegisterOutputConverter( return false; } - auto& stored = output_converters_[def.converter_id]; - stored = def; - if (stored.external_type.empty()) { - stored.external_type = JoinedSlotTypes(stored.external_slots); - } + output_converters_.emplace(def.converter_id, def); return true; } diff --git a/src/cli/alg_pipeline_tool.cpp b/src/cli/alg_pipeline_tool.cpp index b70951b3..7a724a39 100644 --- a/src/cli/alg_pipeline_tool.cpp +++ b/src/cli/alg_pipeline_tool.cpp @@ -653,10 +653,13 @@ int main(int argc, char* argv[]) { {"output_port_mapping", plan->output_port_bindings.All()}, {"effective_max_batch_size", plan->effective_max_batch_size}, {"external_input_type", - plan->input_converter ? plan->input_converter->external_type : ""}, - {"external_output_type", plan->output_converter - ? plan->output_converter->external_type - : ""}}; + plan->input_converter + ? ExternalType(plan->input_converter->external_slots) + : ""}, + {"external_output_type", + plan->output_converter + ? ExternalType(plan->output_converter->external_slots) + : ""}}; nlohmann::json result = {{"schema_version", 1}, {"ok", true}, diff --git a/tests/contract/catalog/test_catalog_contract_ssot.cpp b/tests/contract/catalog/test_catalog_contract_ssot.cpp index a7774f1b..6c42d673 100644 --- a/tests/contract/catalog/test_catalog_contract_ssot.cpp +++ b/tests/contract/catalog/test_catalog_contract_ssot.cpp @@ -318,26 +318,28 @@ TEST_F(CatalogContractSsotTest, IoCatalogExportsKeywordSlotNamesAndTypes) { const auto& input = catalog.at("input_converters").at(0); EXPECT_EQ(input.at("converter_id"), "keyword.plain.operator.v1"); + EXPECT_EQ(input.at("schema_id"), "text.plain.request"); + EXPECT_EQ(input.at("external_type"), "CompanyOperatorKeywordInput"); ASSERT_EQ(input.at("external_slots").size(), 1U); const auto& input_slot = input.at("external_slots").at(0); EXPECT_EQ(input_slot.at("slot_name"), "keyword_in"); EXPECT_EQ(input_slot.at("type_id"), "CompanyOperatorKeywordInput"); EXPECT_EQ(input_slot.at("type_suffix"), "keyword_in"); EXPECT_EQ(input_slot.at("key_suffix"), "keyword_in"); - EXPECT_EQ(input_slot.at("value_type"), "keyword_in"); EXPECT_EQ(input_slot.at("direction"), "input"); EXPECT_EQ(input_slot.at("required"), true); EXPECT_EQ(input_slot.at("capacity_fields"), nlohmann::json::array()); const auto& output = catalog.at("output_converters").at(0); EXPECT_EQ(output.at("converter_id"), "keyword.result.operator.v1"); + EXPECT_EQ(output.at("schema_id"), "keyword.result.response"); + EXPECT_EQ(output.at("external_type"), "CompanyOperatorKeywordOutput"); ASSERT_EQ(output.at("external_slots").size(), 1U); const auto& output_slot = output.at("external_slots").at(0); EXPECT_EQ(output_slot.at("slot_name"), "keyword_out"); EXPECT_EQ(output_slot.at("type_id"), "CompanyOperatorKeywordOutput"); EXPECT_EQ(output_slot.at("type_suffix"), "keyword_out"); EXPECT_EQ(output_slot.at("key_suffix"), "keyword_out"); - EXPECT_EQ(output_slot.at("value_type"), "CompanyOperatorKeywordOutput"); EXPECT_EQ(output_slot.at("direction"), "output"); EXPECT_EQ(output_slot.at("required"), true); EXPECT_EQ(output_slot.at("capacity_fields"), @@ -373,32 +375,16 @@ TEST_F(CatalogContractSsotTest, auto output = *original_output; input.converter_id = "catalog.slot_names.input"; input.external_slots = { - {"request_payload", - "CompanyOperatorKeywordInput", - PortDirection::kInput, - true, - "keyword_in", - "keyword_in", - {}, - "service_request"}, + {"request_payload", "CompanyOperatorKeywordInput", PortDirection::kInput, + true, "keyword_in", "service_request"}, {"fallback_request", "CompanyOperatorKeywordInput", PortDirection::kInput, - false, "keyword_in", "keyword_in"}}; + false, "keyword_in"}}; output.converter_id = "catalog.slot_names.output"; - output.external_slots = {{"reply_payload", - "CompanyOperatorKeywordOutput", - PortDirection::kOutput, - true, - "CompanyOperatorKeywordOutput", - "keyword_out", - {"match_result_json"}, - "service_reply"}, - {"fallback_reply", - "CompanyOperatorKeywordOutput", - PortDirection::kOutput, - false, - "CompanyOperatorKeywordOutput", - "keyword_out", - {"match_result_json"}}}; + output.external_slots = { + {"reply_payload", "CompanyOperatorKeywordOutput", PortDirection::kOutput, + true, "keyword_out", "service_reply"}, + {"fallback_reply", "CompanyOperatorKeywordOutput", PortDirection::kOutput, + false, "keyword_out"}}; ASSERT_TRUE(registry.RegisterInputConverter(input)); ASSERT_TRUE(registry.RegisterOutputConverter(output)); diff --git a/tests/integration/operator/test_operator_api.cpp b/tests/integration/operator/test_operator_api.cpp index 41ae3123..0ec1b9e1 100644 --- a/tests/integration/operator/test_operator_api.cpp +++ b/tests/integration/operator/test_operator_api.cpp @@ -2433,26 +2433,13 @@ const bool g_reg_nested_output_components = []() { OutputConverterDefinition odef; odef.converter_id = "test_nested_output.operator.v1"; - odef.schema_id = "test_nested_output"; - odef.schema_version = 1; - odef.external_type = "test_nested_out"; odef.max_batch_size = 64; - odef.external_slots = {ExternalSlotDefinition{"main", - "test_nested_out", - PortDirection::kOutput, - true, - "result", - "test_nested_out", - {}, - "result"}, - ExternalSlotDefinition{"audit", - "test_nested_out", - PortDirection::kOutput, - true, - "audit", - "test_nested_out", - {}, - "audit"}}; + odef.schema_id = "test_nested_output"; + odef.external_slots = { + ExternalSlotDefinition{"main", "test_nested_out", PortDirection::kOutput, + true, "test_nested_out", "result"}, + ExternalSlotDefinition{"audit", "test_nested_out", PortDirection::kOutput, + true, "test_nested_out", "audit"}}; odef.logical_ports = { NodePortDefinition("rule_matches", "RuleMatchBatch", true, "1:1")}; odef.encode_fn = &EncodeNestedOutput; diff --git a/tests/tooling/studio_browser_test.mjs b/tests/tooling/studio_browser_test.mjs index 6bb0173a..89fc2b97 100644 --- a/tests/tooling/studio_browser_test.mjs +++ b/tests/tooling/studio_browser_test.mjs @@ -123,7 +123,7 @@ try { await page.click("#openRunButton"); for (const label of ["宿主类型 · type_id", "类型注册后缀 · type_suffix", "外部键后缀 · key_suffix"]) { assert.deepEqual(await contractValues(label), ["未提供", "未提供"], - "Missing Catalog fields must not be inferred from slot_name or value_type"); + "Missing Catalog fields must not be inferred from slot_name"); } assert.deepEqual(await contractValues("输入逻辑槽 · slot_name"), ["keyword_in"]); await open("pipeline_browser_multi.json"); diff --git a/tests/unit/adapter/test_adapter_purity.cpp b/tests/unit/adapter/test_adapter_purity.cpp index 7cfc23af..eeeeddac 100644 --- a/tests/unit/adapter/test_adapter_purity.cpp +++ b/tests/unit/adapter/test_adapter_purity.cpp @@ -995,12 +995,14 @@ TEST_F(AdapterPurityTest, ReuseProof_1_InputConverterReusedAcrossBindings) { const auto* entity_conv = IoConverterRegistry::Instance().FindInputConverter( "text.plain.operator.v1"); ASSERT_NE(entity_conv, nullptr); - EXPECT_EQ(entity_conv->external_type, "CompanyOperatorEntityInput"); + EXPECT_EQ(ExternalType(entity_conv->external_slots), + "CompanyOperatorEntityInput"); const auto* keyword_conv = IoConverterRegistry::Instance().FindInputConverter( "keyword.plain.operator.v1"); ASSERT_NE(keyword_conv, nullptr); - EXPECT_EQ(keyword_conv->external_type, "CompanyOperatorKeywordInput"); + EXPECT_EQ(ExternalType(keyword_conv->external_slots), + "CompanyOperatorKeywordInput"); // 用 entity binding 解码输入 { @@ -1108,11 +1110,9 @@ TEST_F(AdapterPurityTest, custom_in_def.converter_id = "test.multi_field.operator.v1"; custom_in_def.schema_id = "multi_field.request"; - custom_in_def.schema_version = 1; - custom_in_def.external_type = "CustomMultiFieldInput"; - custom_in_def.external_slots = {ExternalSlotDefinition( - "inputs", "CustomMultiFieldInput", PortDirection::kInput, true, - "CustomMultiFieldInput", "custom_input")}; + custom_in_def.external_slots = { + ExternalSlotDefinition("inputs", "CustomMultiFieldInput", + PortDirection::kInput, true, "custom_input")}; custom_in_def.max_batch_size = 64; custom_in_def.logical_ports = { NodePortDefinition("texts", "TextBatch", true, "1:1")}; diff --git a/tests/unit/adapter/test_io_binding_registry.cpp b/tests/unit/adapter/test_io_binding_registry.cpp index 020efd71..a7ca6f1a 100644 --- a/tests/unit/adapter/test_io_binding_registry.cpp +++ b/tests/unit/adapter/test_io_binding_registry.cpp @@ -87,11 +87,9 @@ class IoBindingRegistryTest : public ::testing::Test { in_def.converter_id = "test.in.operator"; in_def.schema_id = "in_schema"; - in_def.schema_version = 1; - in_def.external_type = "CompanyOperatorEntityInput"; - in_def.external_slots = {ExternalSlotDefinition( - "entity_in", "CompanyOperatorEntityInput", PortDirection::kInput, true, - "CompanyOperatorEntityInput", "entity_in")}; + in_def.external_slots = { + ExternalSlotDefinition("entity_in", "CompanyOperatorEntityInput", + PortDirection::kInput, true, "entity_in")}; in_def.max_batch_size = 64; in_def.logical_ports = { NodePortDefinition("texts", "TextBatch", true, "1:1")}; @@ -102,11 +100,9 @@ class IoBindingRegistryTest : public ::testing::Test { out_def.converter_id = "test.out.operator"; out_def.schema_id = "out_schema"; - out_def.schema_version = 1; - out_def.external_type = "CompanyOperatorEntityOutput"; - out_def.external_slots = {ExternalSlotDefinition( - "entity_out", "CompanyOperatorEntityOutput", PortDirection::kOutput, - true, "CompanyOperatorEntityOutput", "entity_out", {"entities_json"})}; + out_def.external_slots = { + ExternalSlotDefinition("entity_out", "CompanyOperatorEntityOutput", + PortDirection::kOutput, true, "entity_out")}; out_def.max_batch_size = 64; out_def.logical_ports = { NodePortDefinition("answers", "TextBatch", true, "1:1")}; @@ -303,7 +299,6 @@ TEST_F(IoBindingRegistryTest, CapacityFieldsDeriveFromValueType) { // 保留夹具的逻辑契约,并使用已注册的 keyword 布局。 output.external_slots = { ExternalOutputSlot("keyword_out")}; - output.external_type = "CompanyOperatorKeywordOutput"; ASSERT_TRUE(IoConverterRegistry::Instance().RegisterOutputConverter(output)); const auto catalog = IoCatalog::ToJson(); ASSERT_EQ(catalog["output_converters"].size(), 1U); @@ -319,38 +314,6 @@ TEST_F(IoBindingRegistryTest, CapacityFieldsDeriveFromValueType) { auto unknown = inferred; unknown.type_suffix = "unregistered"; EXPECT_TRUE(EffectiveCapacityFields(unknown).empty()); - - // 同一布局的显式声明与推导声明相互兼容。 - RegisterTestBizBinding(); - auto binding = - *IoBindingRegistry::Instance().FindBinding("test_biz.operator.v1"); - output.converter_id = "explicit.out.operator"; - output.external_slots[0].capacity_fields = {"match_result_json"}; - ASSERT_TRUE(IoConverterRegistry::Instance().RegisterOutputConverter(output)); - binding.binding_id = "explicit.binding"; - binding.output_converter_id = output.converter_id; - ASSERT_TRUE(IoBindingRegistry::Instance().RegisterBinding(binding)); - std::vector errors; - EXPECT_TRUE(IoBindingRegistry::Instance().Audit(&errors)); - EXPECT_TRUE(errors.empty()); -} - -TEST_F(IoBindingRegistryTest, AuditRejectsCapacityFieldsMismatch) { - auto input = - *IoConverterRegistry::Instance().FindInputConverter("test.in.operator"); - auto output = - *IoConverterRegistry::Instance().FindOutputConverter("test.out.operator"); - IoConverterRegistry::Instance().ClearForTesting(); - output.external_slots[0].capacity_fields = {"unknown_capacity"}; - ASSERT_TRUE(IoConverterRegistry::Instance().RegisterInputConverter(input)); - ASSERT_TRUE(IoConverterRegistry::Instance().RegisterOutputConverter(output)); - RegisterTestBizBinding(); - std::vector errors; - EXPECT_FALSE(IoBindingRegistry::Instance().Audit(&errors)); - EXPECT_NE(std::find(errors.begin(), errors.end(), - "Binding 'test_biz.operator.v1' output slot 'entity_out' " - "capacity_fields do not match ValueType 'entity_out'"), - errors.end()); } TEST_F(IoBindingRegistryTest, OmittedPortMappingsUseConverterPortNames) { @@ -1222,7 +1185,6 @@ TEST_F(IoBindingRegistryTest, converter.converter_id = "incompatible.input.converter"; auto& slot = converter.external_slots.front(); slot.type_id = "CompanyOperatorKeywordInput"; - slot.value_type = "CompanyOperatorKeywordInput"; slot.type_suffix = "keyword_in"; slot.key_suffix = "entity_in"; ASSERT_EQ(slot.KeySuffix(), "entity_in"); @@ -1235,10 +1197,8 @@ TEST_F(IoBindingRegistryTest, converter.converter_id = "incompatible.output.converter"; auto& slot = converter.external_slots.front(); slot.type_id = "CompanyOperatorKeywordOutput"; - slot.value_type = "CompanyOperatorKeywordOutput"; slot.type_suffix = "keyword_out"; slot.key_suffix = "entity_out"; - slot.capacity_fields = {"match_result_json"}; ASSERT_EQ(slot.KeySuffix(), "entity_out"); ASSERT_TRUE( IoConverterRegistry::Instance().RegisterOutputConverter(converter)); diff --git a/tests/unit/adapter/test_io_converters.cpp b/tests/unit/adapter/test_io_converters.cpp index af71b0bc..e3cc0689 100644 --- a/tests/unit/adapter/test_io_converters.cpp +++ b/tests/unit/adapter/test_io_converters.cpp @@ -101,10 +101,8 @@ TEST(IoConverterTest, RegisterAndFindInputConverter) { def.converter_id = "test.input.operator.v1"; def.schema_id = "test_input"; - def.schema_version = 1; - def.external_type = "int"; def.external_slots = {ExternalSlotDefinition( - "inputs", "int", PortDirection::kInput, true, "int", "inputs")}; + "inputs", "int", PortDirection::kInput, true, "inputs")}; def.max_batch_size = 64; def.logical_ports = {NodePortDefinition("texts", "TextBatch", true, "1:1")}; def.decode_fn = &DummyDecode; @@ -129,10 +127,8 @@ TEST(IoConverterTest, RegisterAndFindOutputConverter) { def.converter_id = "test.output.operator.v1"; def.schema_id = "test_output"; - def.schema_version = 1; - def.external_type = "int"; def.external_slots = {ExternalSlotDefinition( - "answers", "int", PortDirection::kOutput, true, "int", "answers")}; + "answers", "int", PortDirection::kOutput, true, "answers")}; def.max_batch_size = 64; def.logical_ports = {NodePortDefinition("answers", "TextBatch", true, "1:1")}; def.encode_fn = &DummyEncode; @@ -144,31 +140,14 @@ TEST(IoConverterTest, RegisterAndFindOutputConverter) { EXPECT_EQ(found->converter_id, "test.output.operator.v1"); } -TEST(IoConverterTest, ExternalTypeDefaultsToSlotTypesInOrder) { - InputConverterDefinition input; - input.converter_id = "derived.external_type.in"; - input.schema_id = "test"; - input.external_slots = { +TEST(IoConverterTest, ExternalTypeJoinsSlotTypesInOrder) { + const std::vector slots = { ExternalSlotDefinition("frame", "CompanyFrame", PortDirection::kInput, - true, "frame", "frame"), + true, "frame"), ExternalSlotDefinition("query", "CompanyString", PortDirection::kInput, - true, "string", "query")}; - input.logical_ports = {NodePortDefinition("texts", "TextBatch", true)}; - input.decode_fn = &DummyDecode; - ASSERT_TRUE(IoConverterRegistry::Instance().RegisterInputConverter(input)); - EXPECT_EQ(IoConverterRegistry::Instance() - .FindInputConverter(input.converter_id) - ->external_type, - "CompanyFrame,CompanyString"); - - // 显式的协议标签按声明保留。 - input.converter_id = "labeled.external_type.in"; - input.external_type = "custom.carrier"; - ASSERT_TRUE(IoConverterRegistry::Instance().RegisterInputConverter(input)); - EXPECT_EQ(IoConverterRegistry::Instance() - .FindInputConverter(input.converter_id) - ->external_type, - "custom.carrier"); + true, "query")}; + EXPECT_EQ(ExternalType(slots), "CompanyFrame,CompanyString"); + EXPECT_EQ(ExternalType({}), ""); } TEST(IoConverterTest, RejectsInvalidDefinitions) { @@ -179,10 +158,8 @@ TEST(IoConverterTest, RejectsInvalidDefinitions) { bad_in.converter_id = "bad.in"; bad_in.schema_id = "test"; - bad_in.schema_version = 1; - bad_in.external_type = "int"; bad_in.external_slots = {ExternalSlotDefinition( - "inputs", "int", PortDirection::kInput, true, "int", "inputs")}; + "inputs", "int", PortDirection::kInput, true, "inputs")}; bad_in.max_batch_size = 64; bad_in.logical_ports = { NodePortDefinition("texts", "TextBatch", true, "1:1")}; @@ -194,19 +171,14 @@ TEST(IoConverterTest, RejectsInvalidDefinitions) { bad_in.schema_id = ""; EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); - // schema_version < 1 - bad_in.schema_id = "test"; - bad_in.schema_version = 0; - EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); - // 缺少 external_slots - bad_in.schema_version = 1; + bad_in.schema_id = "test"; bad_in.external_slots.clear(); EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); // 缺少 logical_ports bad_in.external_slots = {ExternalSlotDefinition( - "inputs", "int", PortDirection::kInput, true, "int", "inputs")}; + "inputs", "int", PortDirection::kInput, true, "inputs")}; bad_in.logical_ports.clear(); EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); @@ -221,10 +193,8 @@ TEST(IoConverterTest, RejectsInvalidDefinitions) { bad_out.converter_id = "bad.out"; bad_out.schema_id = "test"; - bad_out.schema_version = 1; - bad_out.external_type = "int"; bad_out.external_slots = {ExternalSlotDefinition( - "answers", "int", PortDirection::kOutput, true, "int", "answers")}; + "answers", "int", PortDirection::kOutput, true, "answers")}; bad_out.max_batch_size = 64; bad_out.logical_ports = { NodePortDefinition("answers", "TextBatch", true, "1:1")}; @@ -294,10 +264,8 @@ TEST(IoConverterTest, bad_in.converter_id = "test.empty_suffix.in"; bad_in.schema_id = "test_schema"; - bad_in.schema_version = 1; - bad_in.external_type = "int"; - bad_in.external_slots = {ExternalSlotDefinition( - "slot1", "int", PortDirection::kInput, true, "int", "")}; + bad_in.external_slots = { + ExternalSlotDefinition("slot1", "int", PortDirection::kInput, true, "")}; bad_in.max_batch_size = 64; bad_in.logical_ports = { NodePortDefinition("texts", "TextBatch", true, "1:1")}; @@ -310,10 +278,8 @@ TEST(IoConverterTest, bad_out.converter_id = "test.empty_suffix.out"; bad_out.schema_id = "test_schema"; - bad_out.schema_version = 1; - bad_out.external_type = "int"; - bad_out.external_slots = {ExternalSlotDefinition( - "slot1", "int", PortDirection::kOutput, true, "int", "")}; + bad_out.external_slots = { + ExternalSlotDefinition("slot1", "int", PortDirection::kOutput, true, "")}; bad_out.max_batch_size = 64; bad_out.logical_ports = { NodePortDefinition("answers", "TextBatch", true, "1:1")}; @@ -326,13 +292,11 @@ TEST(IoConverterTest, multi_in.converter_id = "test.multi_slot.in"; multi_in.schema_id = "test_schema"; - multi_in.schema_version = 1; - multi_in.external_type = "int"; multi_in.external_slots = { ExternalSlotDefinition("slot_first", "int", PortDirection::kInput, true, - "int", "int_suffix"), + "int_suffix"), ExternalSlotDefinition("slot_second", "int", PortDirection::kInput, true, - "int", "int_suffix")}; + "int_suffix")}; multi_in.max_batch_size = 64; multi_in.logical_ports = { NodePortDefinition("texts", "TextBatch", true, "1:1")}; @@ -350,22 +314,11 @@ TEST(IoConverterTest, TEST(IoConverterTest, OptionalInputSlotsPreserveEveryFramePosition) { InputConverterDefinition converter; - converter.external_slots = {{"required", - "CompanyOperatorEntityInput", - PortDirection::kInput, - true, - "CompanyOperatorEntityInput", - "entity_in", - {}, - "required"}, - {"optional", - "CompanyOperatorEntityInput", - PortDirection::kInput, - false, - "CompanyOperatorEntityInput", - "entity_in", - {}, - "optional"}}; + converter.external_slots = { + {"required", "CompanyOperatorEntityInput", PortDirection::kInput, true, + "entity_in", "required"}, + {"optional", "CompanyOperatorEntityInput", PortDirection::kInput, false, + "entity_in", "optional"}}; char text[] = "hello"; CompanyString sentence{5, text}; operator_api::NamedIoBatch inputs(5); @@ -455,7 +408,6 @@ TEST(IoConverterTest, HarnessPreservesNamedSlotsTypesAndPoolCapacities) { TEST(IoConverterTest, HarnessSingleSlotUsesDeclaredSlotType) { InputConverterDefinition input; - input.external_type = "aggregate.input"; input.external_slots = {{"value", "ReproA"}}; input.decode_fn = [](const ExternalInputBatchView& view, const InputDecodeOptions&, const InputPortBindings&, @@ -463,7 +415,6 @@ TEST(IoConverterTest, HarnessSingleSlotUsesDeclaredSlotType) { return view.GetSlot("value", 0) ? 0 : -3; }; OutputConverterDefinition output; - output.external_type = "aggregate.output"; output.external_slots = {{"value", "ReproB", PortDirection::kOutput}}; output.encode_fn = [](AlgContext*, const OutputPortBindings&, const OutputEncodeOptions&, diff --git a/tests/unit/adapter/test_text_converters.cpp b/tests/unit/adapter/test_text_converters.cpp index c8373698..3ad57186 100644 --- a/tests/unit/adapter/test_text_converters.cpp +++ b/tests/unit/adapter/test_text_converters.cpp @@ -168,7 +168,8 @@ TEST_F(TextConvertersTest, ProductionBindingsUseDeclaredHostTypes) { const auto* entity_conv = IoConverterRegistry::Instance().FindInputConverter( entity_binding->input_converter_id); ASSERT_NE(entity_conv, nullptr); - EXPECT_EQ(entity_conv->external_type, "CompanyOperatorEntityInput"); + EXPECT_EQ(ExternalType(entity_conv->external_slots), + "CompanyOperatorEntityInput"); // 关键词匹配经 keyword.plain.operator.v1 使用 // CompanyOperatorKeywordInput @@ -176,7 +177,8 @@ TEST_F(TextConvertersTest, ProductionBindingsUseDeclaredHostTypes) { const auto* keyword_conv = IoConverterRegistry::Instance().FindInputConverter( keyword_binding->input_converter_id); ASSERT_NE(keyword_conv, nullptr); - EXPECT_EQ(keyword_conv->external_type, "CompanyOperatorKeywordInput"); + EXPECT_EQ(ExternalType(keyword_conv->external_slots), + "CompanyOperatorKeywordInput"); // 输出 Converter 互不相同 EXPECT_EQ(entity_binding->output_converter_id, @@ -208,6 +210,55 @@ TEST_F(TextConvertersTest, InputConverterReusedAcrossTestBindings) { EXPECT_EQ(b1->input_converter_id, b2->input_converter_id); } +TEST_F(TextConvertersTest, SameCarrierDifferentPayloadBindingIsRejected) { + // 两个输入转换器槽位与载体完全相同,但对同一请求的解析语义不同。 + const auto decode = [](const char* converter_id, std::string text) { + const auto* conv = + IoConverterRegistry::Instance().FindInputConverter(converter_id); + EXPECT_NE(conv, nullptr); + if (!conv) return std::string(); + CompanyString value{static_cast(text.size()), text.data()}; + CompanyOperatorEntityInput input{1, &value}; + ExternalInputBatchView view; + view.count = 1; + view.slots["entity_in"] = BorrowInputForTest({&input}); + view.slot_types["entity_in"] = "CompanyOperatorEntityInput"; + std::vector request_ids; + InputDecodeOptions options; + options.request_ids = &request_ids; + options.converter_id = conv->converter_id; + AlgContext context; + AdapterStatus status; + EXPECT_EQ(conv->decode_fn( + view, options, + InputPortBindings({{"input_sentences", "input_sentences"}}), + &context, &status), + COMPANY_ALG_SUCCESS); + const auto* sentences = context.Read("input_sentences"); + return sentences && sentences->size() == 1 ? sentences->front().data + : std::string(); + }; + const std::string request = R"({"query":"hello"})"; + ASSERT_EQ(decode("translate.json.operator.v1", request), "hello"); + ASSERT_EQ(decode("text.plain.operator.v1", request), request); + + auto& bindings = IoBindingRegistry::Instance(); + const auto saved = bindings.AllBindings(); + auto alternate = *bindings.FindBinding("translate.operator.v1"); + alternate.binding_id = "translate.plain_text_input"; + alternate.input_converter_id = "text.plain.operator.v1"; + ASSERT_TRUE(bindings.RegisterBinding(alternate)); + + std::string error; + EXPECT_FALSE(bindings.ValidateBizContract("translate", &error)); + EXPECT_NE(error.find("different external I/O contracts"), std::string::npos); + std::vector errors; + EXPECT_FALSE(bindings.Audit(&errors)); + + bindings.ClearForTesting(); + for (const auto& binding : saved) bindings.RegisterBinding(binding); +} + TEST_F(TextConvertersTest, MissingResultsDifferFromOutputCapacityFailures) { const auto* conv = IoConverterRegistry::Instance().FindOutputConverter( "translate.json.operator.v1");