Copilot commented on code in PR #13249:
URL: https://github.com/apache/apisix/pull/13249#discussion_r3099067800
##########
apisix/plugins/ai-proxy/schema.lua:
##########
@@ -133,19 +157,54 @@ local ai_instance_schema = {
}
},
required = {"name", "provider", "auth", "weight"},
- ["if"] = {
- properties = { provider = { enum = { "vertex-ai" } } },
- },
- ["then"] = {
- properties = {
- provider_conf = provider_vertex_ai_schema,
+ allOf = {
+ {
+ ["if"] = {
+ properties = { provider = { enum = { "vertex-ai" } } },
+ },
+ ["then"] = {
+ properties = {
+ provider_conf = provider_vertex_ai_schema,
+ },
+ anyOf = {
+ { required = { "provider_conf" } },
+ { required = { "override" } },
+ },
+ },
+ },
+ {
+ ["if"] = {
+ properties = { provider = { enum = { "bedrock" } } },
+ },
+ ["then"] = {
+ properties = {
+ provider_conf = provider_bedrock_schema,
+ },
+ anyOf = {
+ { required = { "provider_conf" } },
+ { required = { "override" } },
+ },
Review Comment:
For provider="bedrock", the schema currently requires either provider_conf
or override to be present. This prevents a valid configuration where region is
supplied via AWS_REGION (or the provider default) and only auth/options are
configured.
If the implementation supports env/default region (see bedrock.lua),
consider relaxing this anyOf so bedrock can be configured without
provider_conf/override (while still requiring options.model when
override.endpoint is not set).
```suggestion
```
##########
apisix/plugins/ai-protocols/bedrock-converse.lua:
##########
@@ -0,0 +1,283 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You 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.
+--
+
+--- Bedrock Converse protocol adapter (client-side).
+-- Handles detection and response parsing for the Amazon Bedrock
+-- Converse API format. Non-streaming only in this phase.
+
+local core = require("apisix.core")
+local string_sub = string.sub
+local type = type
+local ipairs = ipairs
+local table = table
+
+local _M = {}
+
+
+--- Detect whether the request matches the Bedrock Converse API format.
+-- Uses URI suffix (/converse) and body (valid JSON table with messages).
+function _M.matches(body, ctx)
+ local uri = ctx.var and ctx.var.uri
+ return uri and string_sub(uri, -9) == "/converse"
+ and type(body) == "table" and type(body.messages) == "table"
Review Comment:
matches() treats any request whose URI ends with "/converse" and has a
messages array as Bedrock Converse. This will also match OpenAI Chat-style
bodies (messages with string content) if a route happens to use a "/converse"
suffix, causing protocol mis-detection and confusing "provider ... does not
support bedrock-converse" errors.
Consider tightening detection to Bedrock-specific structure (e.g.,
messages[*].content is an array of {text=...} blocks and/or system is an array
of {text=...}).
```suggestion
local function is_bedrock_text_block_array(value)
if type(value) ~= "table" then
return false
end
for _, block in ipairs(value) do
if type(block) == "table" and type(block.text) == "string" then
return true
end
end
return false
end
local function has_bedrock_converse_shape(body)
if type(body) ~= "table" or type(body.messages) ~= "table" then
return false
end
if is_bedrock_text_block_array(body.system) then
return true
end
for _, message in ipairs(body.messages) do
if type(message) == "table"
and is_bedrock_text_block_array(message.content)
then
return true
end
end
return false
end
--- Detect whether the request matches the Bedrock Converse API format.
-- Uses URI suffix (/converse) and Bedrock-specific content block structure.
function _M.matches(body, ctx)
local uri = ctx.var and ctx.var.uri
return uri and string_sub(uri, -9) == "/converse"
and has_bedrock_converse_shape(body)
```
##########
apisix/plugins/ai-protocols/bedrock-converse.lua:
##########
@@ -0,0 +1,283 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You 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.
+--
+
+--- Bedrock Converse protocol adapter (client-side).
+-- Handles detection and response parsing for the Amazon Bedrock
+-- Converse API format. Non-streaming only in this phase.
+
+local core = require("apisix.core")
+local string_sub = string.sub
+local type = type
+local ipairs = ipairs
+local table = table
+
+local _M = {}
+
+
+--- Detect whether the request matches the Bedrock Converse API format.
+-- Uses URI suffix (/converse) and body (valid JSON table with messages).
+function _M.matches(body, ctx)
+ local uri = ctx.var and ctx.var.uri
+ return uri and string_sub(uri, -9) == "/converse"
+ and type(body) == "table" and type(body.messages) == "table"
+end
+
+
+--- Check whether the request is a streaming request.
+-- Streaming is not supported in this phase.
+function _M.is_streaming(body)
+ return false
+end
+
+
+--- Prepare the outgoing request body for the target provider.
+-- Remove fields Bedrock doesn't accept.
+function _M.prepare_outgoing_request(body)
+ body.stream = nil
+end
+
+
+--- Extract token usage from a non-streaming Bedrock response.
+-- Bedrock format: res_body.usage.inputTokens / outputTokens / totalTokens
+function _M.extract_usage(res_body)
+ if type(res_body) ~= "table" or type(res_body.usage) ~= "table" then
+ return nil, nil
+ end
+ local raw = res_body.usage
+ return {
+ prompt_tokens = raw.inputTokens or 0,
+ completion_tokens = raw.outputTokens or 0,
+ total_tokens = raw.totalTokens
+ or (raw.inputTokens or 0) + (raw.outputTokens or 0),
+ }, raw
+end
+
+
+--- Extract response text from a Bedrock Converse response.
+-- Bedrock format: res_body.output.message.content[].text
+function _M.extract_response_text(res_body)
+ if type(res_body) ~= "table" then
+ return nil
+ end
+ local message = res_body.output and res_body.output.message
+ if type(message) ~= "table" or type(message.content) ~= "table" then
+ return nil
+ end
+ local texts = {}
+ for _, block in ipairs(message.content) do
+ if type(block) == "table" and type(block.text) == "string" then
+ core.table.insert(texts, block.text)
+ end
+ end
+ if #texts > 0 then
+ return table.concat(texts, " ")
+ end
+ return nil
+end
+
+
+--- Extract all text content from a request body for moderation.
+function _M.extract_request_content(body)
+ local contents = {}
+ if type(body.system) == "table" then
+ for _, block in ipairs(body.system) do
+ if type(block) == "table" and type(block.text) == "string" then
+ core.table.insert(contents, block.text)
+ end
+ end
+ end
+ if type(body.messages) == "table" then
+ for _, message in ipairs(body.messages) do
+ if type(message) ~= "table" then
+ goto CONTINUE_MESSAGE
+ end
+ if type(message.content) == "table" then
+ for _, block in ipairs(message.content) do
+ if type(block) == "table" and type(block.text) == "string"
then
+ core.table.insert(contents, block.text)
+ end
+ end
+ end
+ ::CONTINUE_MESSAGE::
+ end
+ end
+ return contents
+end
+
+
+--- Get messages in canonical {role, content} format.
+-- Bedrock content blocks [{text: "..."}] are flattened to plain text.
+function _M.get_messages(body)
+ local messages = {}
+ if type(body.system) == "table" then
+ local texts = {}
+ for _, block in ipairs(body.system) do
+ if type(block) == "table" and type(block.text) == "string" then
+ core.table.insert(texts, block.text)
+ end
+ end
+ if #texts > 0 then
+ core.table.insert(messages, {
+ role = "system",
+ content = table.concat(texts, " "),
+ })
+ end
+ end
+ if type(body.messages) == "table" then
+ for _, message in ipairs(body.messages) do
+ if type(message) ~= "table" then
+ goto CONTINUE
+ end
+ if type(message.content) == "table" then
+ local texts = {}
+ for _, block in ipairs(message.content) do
+ if type(block) == "table" and type(block.text) == "string"
then
+ core.table.insert(texts, block.text)
+ end
+ end
+ if #texts > 0 then
+ core.table.insert(messages, {
+ role = message.role,
+ content = table.concat(texts, " "),
+ })
+ end
+ end
+ ::CONTINUE::
+ end
+ end
+ return messages
+end
+
+
+--- Prepend messages to the request body.
+-- System messages go to body.system; user/assistant messages go to
body.messages.
+function _M.prepend_messages(body, msgs)
+ if not msgs or #msgs == 0 then return end
+
+ local new_system_blocks = {}
+ local new_chat_messages = {}
+ for _, msg in ipairs(msgs) do
+ if msg.role == "system" then
+ core.table.insert(new_system_blocks, {text = msg.content})
+ else
+ core.table.insert(new_chat_messages, {
+ role = msg.role,
+ content = {{text = msg.content}},
+ })
+ end
+ end
+
+ if #new_system_blocks > 0 then
+ if not body.system then
+ body.system = {}
+ end
+ local merged_system = {}
+ for _, block in ipairs(new_system_blocks) do
+ core.table.insert(merged_system, block)
+ end
+ for _, block in ipairs(body.system) do
+ core.table.insert(merged_system, block)
+ end
+ body.system = merged_system
+ end
+
+ if #new_chat_messages > 0 then
+ if not body.messages then
+ body.messages = {}
+ end
+ local merged_messages = {}
+ for _, msg in ipairs(new_chat_messages) do
+ core.table.insert(merged_messages, msg)
+ end
+ for _, msg in ipairs(body.messages) do
+ core.table.insert(merged_messages, msg)
+ end
+ body.messages = merged_messages
+ end
+end
+
+
+--- Append messages to the request body.
+-- System messages go to body.system; user/assistant messages go to
body.messages.
+function _M.append_messages(body, msgs)
+ if not msgs or #msgs == 0 then return end
+
+ for _, msg in ipairs(msgs) do
+ if msg.role == "system" then
+ if not body.system then
+ body.system = {}
+ end
+ core.table.insert(body.system, {text = msg.content})
+ else
+ if not body.messages then
+ body.messages = {}
+ end
+ core.table.insert(body.messages, {
+ role = msg.role,
+ content = {{text = msg.content}},
+ })
+ end
Review Comment:
append_messages() has the same assumption as prepend_messages(): if
body.system/body.messages are present but not tables, core.table.insert(...)
will error. Normalizing these fields to tables before inserting would prevent
request-time exceptions on malformed input.
##########
t/plugin/ai-proxy-bedrock.t:
##########
@@ -0,0 +1,490 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+use t::APISIX 'no_plan';
+
+log_level("info");
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!defined $block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ my $main_config = $block->main_config // <<_EOC_;
+ env AWS_EC2_METADATA_DISABLED=true;
+ env AWS_REGION=us-east-1;
+_EOC_
+ $block->set_value("main_config", $main_config);
+
+ my $user_yaml_config = <<_EOC_;
+plugins:
+ - ai-proxy-multi
+ - prometheus
+_EOC_
+ $block->set_value("extra_yaml_config", $user_yaml_config);
+
+ my $http_config = $block->http_config // <<_EOC_;
+ server {
+ server_name bedrock;
+ listen 6724;
+
+ default_type 'application/json';
+
+ location ~ ^/model/.+/converse\$ {
+ content_by_lua_block {
+ local json = require("cjson.safe")
+
+ if ngx.req.get_method() ~= "POST" then
+ ngx.status = 400
+ ngx.say("Unsupported request method: ",
ngx.req.get_method())
+ return
+ end
+
+ -- Check SigV4 auth headers
+ local auth_header = ngx.req.get_headers()["authorization"]
+ local amz_date = ngx.req.get_headers()["x-amz-date"]
+ if not auth_header or not amz_date then
+ ngx.status = 403
+ ngx.say(json.encode({
+ message = "Missing Authentication Token"
+ }))
+ return
+ end
+
+ -- Capture session token if provided so tests can assert
+ -- that auth.aws.session_token was propagated as
+ -- x-amz-security-token.
+ local session_token =
ngx.req.get_headers()["x-amz-security-token"]
+
+ ngx.req.read_body()
+ local body_data = ngx.req.get_body_data()
+ local body, err = json.decode(body_data)
+
+ if not body then
+ ngx.status = 400
+ ngx.say(json.encode({ message = "Invalid JSON: " ..
(err or "") }))
+ return
+ end
+
+ -- Verify model is NOT in the body (remove_model = true)
+ if body.model then
+ ngx.status = 400
+ ngx.say(json.encode({
+ message = "model field should not be in request
body"
+ }))
+ return
+ end
+
+ -- Verify request has messages
+ if not body.messages or #body.messages < 1 then
+ ngx.status = 400
+ ngx.say(json.encode({ message = "messages is required"
}))
+ return
+ end
+
+ -- Extract text from first user message
+ local first_content = ""
+ for _, msg in ipairs(body.messages) do
+ if msg.role == "user" and msg.content then
+ for _, block in ipairs(msg.content) do
+ if block.text then
+ first_content = block.text
+ break
+ end
+ end
+ break
+ end
+ end
+
Review Comment:
The variable first_content is computed but never used. Removing it (and the
loop that populates it) would reduce noise in the test stub and make the intent
of the mock handler clearer.
```suggestion
```
##########
apisix/plugins/ai-protocols/bedrock-converse.lua:
##########
@@ -0,0 +1,283 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You 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.
+--
+
+--- Bedrock Converse protocol adapter (client-side).
+-- Handles detection and response parsing for the Amazon Bedrock
+-- Converse API format. Non-streaming only in this phase.
+
+local core = require("apisix.core")
+local string_sub = string.sub
+local type = type
+local ipairs = ipairs
+local table = table
+
+local _M = {}
+
+
+--- Detect whether the request matches the Bedrock Converse API format.
+-- Uses URI suffix (/converse) and body (valid JSON table with messages).
+function _M.matches(body, ctx)
+ local uri = ctx.var and ctx.var.uri
+ return uri and string_sub(uri, -9) == "/converse"
+ and type(body) == "table" and type(body.messages) == "table"
+end
+
+
+--- Check whether the request is a streaming request.
+-- Streaming is not supported in this phase.
+function _M.is_streaming(body)
+ return false
+end
+
+
+--- Prepare the outgoing request body for the target provider.
+-- Remove fields Bedrock doesn't accept.
+function _M.prepare_outgoing_request(body)
+ body.stream = nil
+end
+
+
+--- Extract token usage from a non-streaming Bedrock response.
+-- Bedrock format: res_body.usage.inputTokens / outputTokens / totalTokens
+function _M.extract_usage(res_body)
+ if type(res_body) ~= "table" or type(res_body.usage) ~= "table" then
+ return nil, nil
+ end
+ local raw = res_body.usage
+ return {
+ prompt_tokens = raw.inputTokens or 0,
+ completion_tokens = raw.outputTokens or 0,
+ total_tokens = raw.totalTokens
+ or (raw.inputTokens or 0) + (raw.outputTokens or 0),
+ }, raw
+end
+
+
+--- Extract response text from a Bedrock Converse response.
+-- Bedrock format: res_body.output.message.content[].text
+function _M.extract_response_text(res_body)
+ if type(res_body) ~= "table" then
+ return nil
+ end
+ local message = res_body.output and res_body.output.message
+ if type(message) ~= "table" or type(message.content) ~= "table" then
+ return nil
+ end
+ local texts = {}
+ for _, block in ipairs(message.content) do
+ if type(block) == "table" and type(block.text) == "string" then
+ core.table.insert(texts, block.text)
+ end
+ end
+ if #texts > 0 then
+ return table.concat(texts, " ")
+ end
+ return nil
+end
+
+
+--- Extract all text content from a request body for moderation.
+function _M.extract_request_content(body)
+ local contents = {}
+ if type(body.system) == "table" then
+ for _, block in ipairs(body.system) do
+ if type(block) == "table" and type(block.text) == "string" then
+ core.table.insert(contents, block.text)
+ end
+ end
+ end
+ if type(body.messages) == "table" then
+ for _, message in ipairs(body.messages) do
+ if type(message) ~= "table" then
+ goto CONTINUE_MESSAGE
+ end
+ if type(message.content) == "table" then
+ for _, block in ipairs(message.content) do
+ if type(block) == "table" and type(block.text) == "string"
then
+ core.table.insert(contents, block.text)
+ end
+ end
+ end
+ ::CONTINUE_MESSAGE::
+ end
+ end
+ return contents
+end
+
+
+--- Get messages in canonical {role, content} format.
+-- Bedrock content blocks [{text: "..."}] are flattened to plain text.
+function _M.get_messages(body)
+ local messages = {}
+ if type(body.system) == "table" then
+ local texts = {}
+ for _, block in ipairs(body.system) do
+ if type(block) == "table" and type(block.text) == "string" then
+ core.table.insert(texts, block.text)
+ end
+ end
+ if #texts > 0 then
+ core.table.insert(messages, {
+ role = "system",
+ content = table.concat(texts, " "),
+ })
+ end
+ end
+ if type(body.messages) == "table" then
+ for _, message in ipairs(body.messages) do
+ if type(message) ~= "table" then
+ goto CONTINUE
+ end
+ if type(message.content) == "table" then
+ local texts = {}
+ for _, block in ipairs(message.content) do
+ if type(block) == "table" and type(block.text) == "string"
then
+ core.table.insert(texts, block.text)
+ end
+ end
+ if #texts > 0 then
+ core.table.insert(messages, {
+ role = message.role,
+ content = table.concat(texts, " "),
+ })
+ end
+ end
+ ::CONTINUE::
+ end
+ end
+ return messages
+end
+
+
+--- Prepend messages to the request body.
+-- System messages go to body.system; user/assistant messages go to
body.messages.
+function _M.prepend_messages(body, msgs)
+ if not msgs or #msgs == 0 then return end
+
+ local new_system_blocks = {}
+ local new_chat_messages = {}
+ for _, msg in ipairs(msgs) do
+ if msg.role == "system" then
+ core.table.insert(new_system_blocks, {text = msg.content})
+ else
+ core.table.insert(new_chat_messages, {
+ role = msg.role,
+ content = {{text = msg.content}},
+ })
+ end
+ end
+
+ if #new_system_blocks > 0 then
+ if not body.system then
+ body.system = {}
+ end
+ local merged_system = {}
+ for _, block in ipairs(new_system_blocks) do
+ core.table.insert(merged_system, block)
+ end
+ for _, block in ipairs(body.system) do
+ core.table.insert(merged_system, block)
+ end
+ body.system = merged_system
Review Comment:
prepend_messages() assumes body.system and body.messages are tables when
they already exist. If a client sends an invalid Bedrock body where
system/messages are present but not arrays, this will raise (ipairs/insert on
non-table) instead of failing gracefully.
Consider normalizing these fields before use (e.g., if type(body.system) ~=
"table" then body.system = {} end; same for body.messages) to avoid runtime
errors.
##########
apisix/plugins/ai-transport/auth-aws.lua:
##########
@@ -0,0 +1,134 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You 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.
+--
+
+--- AWS SigV4 signing helper for AI providers.
+-- Signs outgoing HTTP requests using AWS Signature Version 4.
+
+require("resty.aws.config") -- reads env vars before init
+local aws = require("resty.aws")
+local core = require("apisix.core")
+local signer = require("resty.aws.request.sign")
+local ngx_escape_uri = ngx.escape_uri
+
+local aws_instance
+
+
+-- Encode a URL path for AWS SigV4 canonical URI.
+-- AWS SigV4 requires each path segment to be URI-encoded twice. This function
+-- applies a single ngx.escape_uri pass per segment; the second encoding pass
+-- comes from the caller's input, which is expected to already be URL-encoded
+-- (e.g. bedrock.lua escapes the model ID before building the path, turning
+-- raw ":" into "%3A"). Running ngx.escape_uri here then escapes the "%" to
+-- "%25", yielding the "%253A" required by the canonical URI.
+local function encode_path_for_canonical_uri(path)
+ local segments = {}
+ for segment in path:gmatch("[^/]+") do
+ -- Encodes any unreserved chars and re-escapes "%" from the upstream
+ -- encoding pass, producing the double-encoded form SigV4 expects.
+ segments[#segments + 1] = ngx_escape_uri(segment)
Review Comment:
encode_path_for_canonical_uri() assumes params.path is already URL-encoded
once (so a single escape pass yields the required double-encoding). When users
configure bedrock via override.endpoint with a raw path (common, and also used
in the tests), segments containing ':' or other reserved characters will only
be encoded once in canonicalURI, producing an invalid SigV4 signature.
Consider deriving canonicalURI by normalizing each segment from its decoded
form (e.g., ngx.unescape_uri(segment)) and then applying URI-encoding twice, so
both pre-encoded and raw paths sign correctly.
```suggestion
local ngx_unescape_uri = ngx.unescape_uri
local aws_instance
-- Encode a URL path for AWS SigV4 canonical URI.
-- AWS SigV4 requires each path segment to be URI-encoded twice. Normalize
-- each segment from its decoded form first so both raw paths (for example
-- "model:invoke") and already encoded paths (for example "model%3Ainvoke")
-- produce the same canonical URI segment ("%253A" for ":").
local function encode_path_for_canonical_uri(path)
local segments = {}
for segment in path:gmatch("[^/]+") do
local normalized_segment = ngx_unescape_uri(segment)
segments[#segments + 1] =
ngx_escape_uri(ngx_escape_uri(normalized_segment))
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]