项目文件夹

文件
2026-07-13 12:32:21 +08:00

335 行
9.9 KiB
Lua

local _M = {}
-- imports
local cjson = require("cjson.safe")
local fmt = string.format
local ai_shared = require("kong.llm.drivers.shared")
local socket_url = require("socket.url")
local ai_plugin_ctx = require("kong.llm.plugin.ctx")
--
local DRIVER_NAME = "huggingface"
function _M.pre_request(conf, body)
return true, nil
end
local function from_huggingface(response_string, model_info, route_type)
local response_table, err = cjson.decode(response_string)
if not response_table then
ngx.log(ngx.ERR, "Failed to decode JSON response from HuggingFace API: ", err)
return nil, "Failed to decode response"
end
if response_table.error or response_table.message then
local error_msg = response_table.error or response_table.message
ngx.log(ngx.ERR, "Error from HuggingFace API: ", error_msg)
return nil, "API error: " .. error_msg
end
local transformed_response = {
model = model_info.name,
object = response_table.object or route_type,
choices = {},
usage = {},
}
-- Chat reports usage, generation does not
transformed_response.usage = response_table.usage or {}
response_table.generated_text = response_table[1] and response_table[1].generated_text or nil
if response_table.generated_text then
table.insert(transformed_response.choices, {
message = { content = response_table.generated_text },
index = 0,
finish_reason = "complete",
})
elseif response_table.choices then
for i, choice in ipairs(response_table.choices) do
local content = choice.message and choice.message.content or ""
table.insert(transformed_response.choices, {
message = { content = content },
index = i - 1,
finish_reason = "complete",
})
end
else
ngx.log(ngx.ERR, "Unexpected response format from Hugging Face API")
return nil, "Invalid response format"
end
local result_string, err = cjson.encode(transformed_response)
if not result_string then
ngx.log(ngx.ERR, "Failed to encode transformed response: ", err)
return nil, "Failed to encode response"
end
return result_string, nil
end
local function set_huggingface_options(model_info)
local use_cache = false
local wait_for_model = false
if model_info and model_info.options and model_info.options.huggingface then
use_cache = model_info.options.huggingface.use_cache or false
wait_for_model = model_info.options.huggingface.wait_for_model or false
end
return {
use_cache = use_cache,
wait_for_model = wait_for_model,
}
end
local function set_default_parameters(request_table)
local parameters = request_table.parameters or {}
if parameters.top_k == nil then
parameters.top_k = request_table.top_k
end
if parameters.top_p == nil then
parameters.top_p = request_table.top_p
end
if parameters.temperature == nil then
parameters.temperature = request_table.temperature
end
if parameters.max_tokens == nil then
if request_table.messages then
-- conversational model use the max_length param
-- https://huggingface.co/docs/api-inference/en/detailed_parameters?code=curl#conversational-task
parameters.max_length = request_table.max_tokens
else
parameters.max_new_tokens = request_table.max_tokens
end
end
request_table.top_k = nil
request_table.top_p = nil
request_table.temperature = nil
request_table.max_tokens = nil
return parameters
end
local function to_huggingface(task, request_table, model_info)
local parameters = set_default_parameters(request_table)
local options = set_huggingface_options(model_info)
if task == "llm/v1/completions" then
request_table.inputs = request_table.prompt
request_table.prompt = nil
end
request_table.options = options
request_table.parameters = parameters
request_table.model = model_info.name or request_table.model
return request_table, "application/json", nil
end
local function safe_access(tbl, ...)
local value = tbl
for _, key in ipairs({ ... }) do
value = value and value[key]
if not value then
return nil
end
end
return value
end
local function handle_huggingface_stream(event_t, model_info, route_type)
-- discard empty frames, it should either be a random new line, or comment
if (not event_t.data) or (#event_t.data < 1) then
return
end
local event, err = cjson.decode(event_t.data)
if err then
ngx.log(ngx.WARN, "failed to decode stream event frame from Hugging Face: ", err)
return nil, "failed to decode stream event frame from Hugging Face", nil
end
local new_event
if route_type == "stream/llm/v1/chat" then
local content = safe_access(event, "choices", 1, "delta", "content") or ""
new_event = {
choices = {
[1] = {
delta = {
content = content,
role = "assistant",
},
index = 0,
},
},
model = event.model or model_info.name,
object = "chat.completion.chunk",
}
else
local text = safe_access(event, "token", "text") or ""
new_event = {
choices = {
[1] = {
text = text,
index = 0,
},
},
model = model_info.name,
object = "text_completion",
}
end
return cjson.encode(new_event), nil, nil
end
local transformers_from = {
["llm/v1/chat"] = from_huggingface,
["llm/v1/completions"] = from_huggingface,
["stream/llm/v1/chat"] = handle_huggingface_stream,
["stream/llm/v1/completions"] = handle_huggingface_stream,
}
function _M.from_format(response_string, model_info, route_type)
ngx.log(ngx.DEBUG, "converting from ", model_info.provider, "://", route_type, " type to kong")
-- MUST return a string, set as the response body
if not transformers_from[route_type] then
return nil, fmt("no transformer available from format %s://%s", model_info.provider, route_type)
end
local ok, response_string, err, metadata =
pcall(transformers_from[route_type], response_string, model_info, route_type)
if not ok or err then
return nil,
fmt("transformation failed from type %s://%s: %s", model_info.provider, route_type, err or "unexpected_error")
end
return response_string, nil, metadata
end
local transformers_to = {
["llm/v1/chat"] = to_huggingface,
["llm/v1/completions"] = to_huggingface,
}
function _M.to_format(request_table, model_info, route_type)
if not transformers_to[route_type] then
return nil, nil, fmt("no transformer for %s://%s", model_info.provider, route_type)
end
request_table = ai_shared.merge_config_defaults(request_table, model_info.options, model_info.route_type)
local ok, response_object, content_type, err =
pcall(transformers_to[route_type], route_type, request_table, model_info)
if err or not ok then
return nil, nil, fmt("error transforming to %s://%s", model_info.provider, route_type)
end
return response_object, content_type, nil
end
local function build_url(base_url, route_type)
return (route_type == "llm/v1/completions") and base_url or (base_url .. "/v1/chat/completions")
end
local function huggingface_endpoint(conf, model)
local parsed_url
local base_url
if model.options and model.options.upstream_url then
base_url = model.options.upstream_url
elseif model.name then
base_url = fmt(ai_shared.upstream_url_format[DRIVER_NAME], model.name)
else
return nil
end
local url = build_url(base_url, conf.route_type)
parsed_url = socket_url.parse(url)
return parsed_url
end
function _M.configure_request(conf)
local model = ai_plugin_ctx.get_request_model_table_inuse()
if not model or type(model) ~= "table" or model.provider ~= DRIVER_NAME then
return nil, "invalid model parameter"
end
local parsed_url = huggingface_endpoint(conf, model)
if not parsed_url then
return kong.response.exit(400, "Could not parse the Hugging Face model endponit")
end
if parsed_url.path then
kong.service.request.set_path(parsed_url.path)
end
kong.service.request.set_scheme(parsed_url.scheme)
kong.service.set_target(parsed_url.host, tonumber(parsed_url.port) or 443)
local auth_header_name = conf.auth and conf.auth.header_name
local auth_header_value = conf.auth and conf.auth.header_value
if auth_header_name and auth_header_value then
kong.service.request.set_header(auth_header_name, auth_header_value)
end
return true, nil
end
function _M.post_request(conf)
-- Clear any response headers if needed
if ai_shared.clear_response_headers[DRIVER_NAME] then
for i, v in ipairs(ai_shared.clear_response_headers[DRIVER_NAME]) do
kong.response.clear_header(v)
end
end
end
function _M.subrequest(body, conf, http_opts, return_res_table)
-- Encode the request body as JSON
local body_string, err = cjson.encode(body)
if not body_string then
return nil, nil, "Failed to encode body to JSON: " .. (err or "unknown error")
end
-- Construct the Hugging Face API URL
local url = huggingface_endpoint(conf)
if not url then
return nil, nil, "Could not parse the Hugging Face model endpoint"
end
local url_string = url.scheme .. "://" .. url.host .. (url.path or "")
local headers = {
["Accept"] = "application/json",
["Content-Type"] = "application/json",
}
if conf.auth and conf.auth.header_name then
headers[conf.auth.header_name] = conf.auth.header_value
end
local method = "POST"
local res, err, httpc = ai_shared.http_request(url_string, body_string, method, headers, http_opts, return_res_table)
-- Handle the response
if not res then
return nil, nil, "Request to Hugging Face API failed: " .. (err or "unknown error")
end
-- Check if the response should be returned as a table
if return_res_table then
return {
status = res.status,
headers = res.headers,
body = res.body,
},
res.status,
nil,
httpc
else
if res.status >= 200 and res.status < 300 then
return res.body, res.status, nil
else
return res.body, res.status, "Hugging Face API returned status " .. res.status
end
end
end
return _M