项目文件夹

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

360 行
11 KiB
Lua

local cjson = require("cjson.safe")
local fmt = string.format
local _GeminiAdapter = {}
_GeminiAdapter.role_map = {
["human"] = "user",
["model"] = "assistant",
["function"] = "tool",
}
-- Creates a new GeminiAdapter.
-- @param o (table or nil) The object to create the adapter from.
function _GeminiAdapter:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
-- The format ID for the adapter.
self.FORMAT_ID = "gemini"
-- The LLM provider drivers compatible with the adapter.
self.PROVIDERS_COMPATIBLE = {
["gemini"] = true,
}
return o
end
-- Determines if the adapter is compatible with a provider.
-- @param provider (string) The provider to check compatibility with.
-- @return (boolean) True if compatible, false otherwise.
function _GeminiAdapter:is_compatible(provider)
return self.PROVIDERS_COMPATIBLE[provider]
end
-- Extracts metadata from a Gemini response body and returns a table of fields to add to analytics.
-- @param response_body (string) The Gemini response body.
-- @return res (table) The Kong AI Gateway response metadata object.
function _GeminiAdapter:extract_metadata(response_body)
if response_body then
local err
response_body, err = cjson.decode(response_body)
if err then
return nil, err
end
if response_body.usageMetadata then
return {
prompt_tokens = response_body.usageMetadata.promptTokenCount or 0,
completion_tokens = response_body.usageMetadata.candidatesTokenCount or 0,
}
end
end
return {
prompt_tokens = 0,
completion_tokens = 0,
}
end
-- Extracts the response model version from a Gemini response table.
-- @param response_body (table) The Gemini response table.
-- @return model_version (string) The model version.
function _GeminiAdapter:extract_response_model(response_body)
if response_body then
local err
response_body, err = cjson.decode(response_body)
if err then
return nil, err
end
return response_body.modelVersion
end
return nil
end
-- Converts a Gemini part to an OpenAI part.
-- @param part (table) The Gemini part.
-- @return new_part (table) The OpenAI part.
function _GeminiAdapter:gemini_part_to_openai_part(part)
if part.text then
return {
["type"] = "text",
["text"] = part.text
}
elseif part.inline_data then
local mimetype = part.inline_data.mime_type
local data = part.inline_data.data
return {
["type"] = "image_url",
["image_url"] = {
["url"] = fmt("data:%s;base64,%s", mimetype, data)
}
}
elseif part.file_data then
-- TODO handle this better
-- OpenAI only supports image_url for the chat endpoints right now
-- but Gemini supports audio, video, and others.
--
-- We'll have to just assumed it's image_url and wait for OpenAI
-- support later.
--
-- This WON'T break the parser or the native request, it will just
-- look weird in the logs.
local file_uri = part.file_data.file_uri
return {
["type"] = "image_url",
["image_url"] = {
["url"] = file_uri
}
}
elseif part.functionCall then
return {
["type"] = "function",
["function"] = {
["name"] = part.functionCall.name,
["id"] = part.functionCall.name,
["arguments"] = cjson.encode(part.functionCall.args),
},
}
end
end
-- Converts a Gemini message to a Kong message.
-- @param msg (table) The Gemini message.
-- @return new_msg (table) The Kong message.
function _GeminiAdapter:gemini_msg_to_openai_msg(msg)
local new_msg = {}
if msg.role and type(msg.role) == "string" then
new_msg.role = _GeminiAdapter.role_map[msg.role] or msg.role
end
if msg.parts
and type(msg.parts) == "table" then
new_msg.content = nil
new_msg.tool_calls = nil
-- handle parts-by-key, and array-of-parts, differently
if #msg.parts > 0 then
for _, v in ipairs(msg.parts) do
local part = self:gemini_part_to_openai_part(v)
-- this is a special case
if v.functionCall then
new_msg.tool_calls = new_msg.tool_calls or {}
table.insert(new_msg.tool_calls, part)
elseif v.function_response then
new_msg.content = v.function_response.response and v.function_response.response.content
else
new_msg.content = new_msg.content or {}
table.insert(new_msg.content, part)
end
end
elseif next(msg.parts) then
-- this is a special case
if msg.parts.functionCall then
new_msg.tool_calls = new_msg.tool_calls or {}
table.insert(new_msg.tool_calls, msg.parts.functionCall)
elseif msg.parts.function_response then
-- special case, replaces the whole message
new_msg.content = cjson.encode(msg.parts.function_response.response and msg.parts.function_response.response.content)
else
new_msg.content = new_msg.content or {}
table.insert(new_msg.content, self:gemini_part_to_openai_part(msg.parts))
end
end
end
return new_msg
end
-- Converts a Gemini contents table to a Kong messages table.
-- @param response_table (table) The Gemini contents table.
-- @return res (table) The Kong messages table.
function _GeminiAdapter:extract_messages(contents, system_instruction)
local messages
for _, msg in ipairs(contents) do
messages = messages or {}
table.insert(messages, self:gemini_msg_to_openai_msg(msg))
end
-- handle the system prompt differently for Gemini
if system_instruction then
local system_text
if #system_instruction.parts > 0 then
system_text = system_instruction.parts[1].text
elseif next(system_instruction.parts) then
system_text = system_instruction.parts.text
end
if system_text then
table.insert(messages, 1, { role = "system", content = { { type = "text", text = system_text } } })
end
end
return messages
end
-- Updates the native Gemini request table with the given configuration.
-- @param native_request_t (table) The native Gemini request table.
-- @param conf_m (table) The configuration table.
function _GeminiAdapter:update_inference_parameters(native_request_t, conf_m)
-- for performance, we only need to decode and encode the body
-- if something actually changes
native_request_t.generationConfig = native_request_t.generationConfig or {}
native_request_t.generationConfig.temperature = conf_m.model.options and conf_m.model.options.temperature or native_request_t.generationConfig.temperature
native_request_t.generationConfig.maxOutputTokens = conf_m.model.options and conf_m.model.options.max_tokens or native_request_t.generationConfig.maxOutputTokens
native_request_t.generationConfig.topP = conf_m.model.options and conf_m.model.options.top_p or native_request_t.generationConfig.topP
native_request_t.generationConfig.topK = conf_m.model.options and conf_m.model.options.top_k or native_request_t.generationConfig.topK
end
-- Extracts metadata from a Gemini request table and returns a table of fields to add to converted Kong request.
-- @param request_table (table) The Gemini generationConfig from the request.
-- @return req (table) The Kong AI Gateway response metadata object.
function _GeminiAdapter:extract_inference_parameters(generationConfig)
if generationConfig then
local openai_parameters = {}
openai_parameters.temperature = generationConfig.temperature
openai_parameters.max_tokens = generationConfig.maxOutputTokens
openai_parameters.top_p = generationConfig.topP
openai_parameters.top_k = generationConfig.topK
openai_parameters.stop = generationConfig.stopSequences
return openai_parameters
end
end
-- Extracts the model name and whether it is streaming, from the incoming coordinates.
-- @param path (string) The request path.
-- @param uri_captures (table) The URI captures.
-- @return model_name (string) The model name.
-- @return stream (boolean) Whether the response should stream.
function _GeminiAdapter:extract_model_and_stream(path, uri_captures)
-- try named URI captures first
local model_name = uri_captures.named and uri_captures.named.model
local operation = uri_captures.named and uri_captures.named.operation
-- otherwise try raw parsing the path,
-- in case the user has set this up incorrectly
-- TODO: also consider upstream_url?
if (not model_name) or (not operation) then
local t_model_name, t_operation = path:match("/models/([^:]+):([^/]+)$")
model_name = t_model_name or model_name
operation = t_operation or operation
end
return model_name, (operation == "streamGenerateContent" and true) or false
end
-- Extracts tools from a Gemini request table and returns a table of fields to add to converted Kong request.
-- @param request_table (table) The Gemini tools from the request.
-- @return req (table) The Kong AI Gateway response metadata object.
function _GeminiAdapter:extract_tools(tools)
local openai_tools = {}
for _, tool in ipairs(tools[1].function_declarations) do
local new_tool = {
["type"] = "function",
["function"] = tool,
}
-- TODO any customisation here, looks like there is none
-- it's just a standard jsonschema snippet
table.insert(openai_tools, new_tool)
end
return openai_tools
end
-- Converts a Gemini request table to a Kong request table.
-- @param request_table (table) The Gemini request table.
-- @return req (table) The Kong request table.
function _GeminiAdapter:to_kong_req(gemini_table, kong)
local openai_table = {}
-- try to capture the model from the request path
-- otherwise we'll use the model name from the plugin config
-- otherwise we'll fail the request
openai_table.model, openai_table.stream = self:extract_model_and_stream(kong.request.get_path(), kong.request.get_uri_captures())
if gemini_table.contents
and type(gemini_table.contents) == "table" then
if #gemini_table.contents > 0 then
-- convert messages
openai_table.messages = self:extract_messages(gemini_table.contents, gemini_table.system_instruction)
elseif next(gemini_table.contents) then
openai_table.messages = {
self:gemini_msg_to_openai_msg(gemini_table.contents)
}
end
end
-- convert tuning parameters
if gemini_table.generationConfig then
for k, v in pairs(self:extract_inference_parameters(gemini_table.generationConfig)) do
openai_table[k] = v
end
end
-- finally handle tool definitions
if gemini_table.tools
and type(gemini_table.tools) == "table"
and #gemini_table.tools > 0
and type(gemini_table.tools[1]) == "table"
and gemini_table.tools[1].function_declarations
and #gemini_table.tools[1].function_declarations > 0 then
openai_table.tools = self:extract_tools(gemini_table.tools)
end
return openai_table
end
-- for unit tests
if _G.TEST then
_GeminiAdapter._set_kong = function(this_kong)
_G.kong = this_kong
end
_GeminiAdapter._get_kong = function()
return kong
end
end
return _GeminiAdapter