Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion lua/gp/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ local config = {
endpoint = "https://api.openai.com/v1/chat/completions",
-- secret = os.getenv("OPENAI_API_KEY"),
},
openai_resp = {
disable = false,
endpoint = "https://api.openai.com/v1/responses",
secret = os.getenv("OPENAI_API_KEY"),
},
azure = {
disable = true,
endpoint = "https://$URL.openai.azure.com/openai/deployments/{{model}}/chat/completions",
Expand All @@ -59,7 +64,8 @@ local config = {
},
googleai = {
disable = true,
endpoint = "https://generativelanguage.googleapis.com/v1beta/models/{{model}}:streamGenerateContent?key={{secret}}",
endpoint =
"https://generativelanguage.googleapis.com/v1beta/models/{{model}}:streamGenerateContent?key={{secret}}",
secret = os.getenv("GOOGLEAI_API_KEY"),
},
pplx = {
Expand Down Expand Up @@ -111,6 +117,16 @@ local config = {
-- system prompt (use this to specify the persona/role of the AI)
system_prompt = require("gp.defaults").chat_system_prompt,
},
{
name = "ChatGPT4o_new",
provider = "openai_resp",
chat = true,
command = false,
-- string with model name or table with model name and parameters
model = { model = "gpt-4o", temperature = 1.1, top_p = 1 },
-- system prompt (use this to specify the persona/role of the AI)
system_prompt = require("gp.defaults").chat_system_prompt,
},
{
provider = "openai",
name = "ChatGPT4o-mini",
Expand Down Expand Up @@ -368,6 +384,9 @@ local config = {
---@type "popup" | "split" | "vsplit" | "tabnew"
toggle_target = "vsplit",

-- utilize tooling made available in the openai responses API, see openAI's online documentation
openai_resp_tools = {},

-- styling for chatfinder
---@type "single" | "double" | "rounded" | "solid" | "shadow" | "none"
style_chat_finder_border = "single",
Expand Down
84 changes: 83 additions & 1 deletion lua/gp/dispatcher.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ D.setup = function(opts)

D.config.curl_params = opts.curl_params or default_config.curl_params

D.config.openai_resp_tools = opts.openai_resp_tools or default_config.openai_resp_tools

D.providers = vim.deepcopy(default_config.providers)
opts.providers = opts.providers or {}
for k, v in pairs(opts.providers) do
Expand Down Expand Up @@ -212,6 +214,17 @@ D.prepare_payload = function(messages, model, provider)
temperature = math.max(0, math.min(2, model.temperature or 1)),
top_p = math.max(0, math.min(1, model.top_p or 1)),
}
if provider == "openai_resp" then
output = {
model = model.model,
stream = true,
input = messages,
tools = D.config.openai_resp_tools,
max_output_tokens = model.max_completion_tokens or 4096,
temperature = math.max(0, math.min(2, model.temperature or 1)),
top_p = math.max(0, math.min(1, model.top_p or 1)),
}
end

if (provider == "openai" or provider == "copilot") and model.model:sub(1, 1) == "o" then
if model.model:sub(1, 2) == "o3" then
Expand All @@ -229,7 +242,7 @@ D.prepare_payload = function(messages, model, provider)
output.top_p = nil
end

if model.model == "gpt-5" or model.model == "gpt-5-mini" then
if model.model == "gpt-5" or model.model == "gpt-5-mini" then
-- remove max_tokens, top_p, temperature for gpt-5 models (duh)
output.max_tokens = nil
output.temperature = nil
Expand Down Expand Up @@ -289,6 +302,38 @@ local query = function(buf, provider, payload, handler, on_exit, callback)
end
line = line:gsub("^data: ", "")
local content = ""

if qt.provider == "openai_resp" then
local ok, evt = pcall(vim.json.decode, line)
if ok and type(evt) == "table" and evt.type then
if evt.type == "response.output_text.delta"
and type(evt.delta) == "string"
then
-- Streamed token chunk
content = evt.delta
elseif evt.type == "response.completed"
and qt.response == ""
and evt.response
then
-- Fallback for non-stream / if deltas weren't processed.
local resp = evt.response
if type(resp.output) == "table" then
local acc = {}
for _, item in ipairs(resp.output) do
if item.type == "message" and type(item.content) == "table" then
for _, part in ipairs(item.content) do
if part.type == "output_text" and type(part.text) == "string" then
table.insert(acc, part.text)
end
end
end
end
content = table.concat(acc)
end
end
end
end

if line:match("choices") and line:match("delta") and line:match("content") then
line = vim.json.decode(line)
if line.choices[1] and line.choices[1].delta and line.choices[1].delta.content then
Expand Down Expand Up @@ -379,6 +424,38 @@ local query = function(buf, provider, payload, handler, on_exit, callback)
handler(qid, content)
end
end
if qt.provider == "openai_resp" and content == "" then
local last_json
for json_str in raw_response:gmatch("data:%s*(%b{})") do
last_json = json_str
end

if last_json then
local ok, evt = pcall(vim.json.decode, last_json)
if ok and evt
and evt.type == "response.completed"
and evt.response
and type(evt.response.output) == "table"
then
local acc = {}
for _, item in ipairs(evt.response.output) do
if item.type == "message" and type(item.content) == "table" then
for _, part in ipairs(item.content) do
if part.type == "output_text" and type(part.text) == "string" then
table.insert(acc, part.text)
end
end
end
end
local full = table.concat(acc)
if full ~= "" then
qt.response = qt.response .. full
handler(qid, full)
content = qt.response
end
end
end
end


if qt.response == "" then
Expand Down Expand Up @@ -434,6 +511,11 @@ local query = function(buf, provider, payload, handler, on_exit, callback)
"-H",
"api-key: " .. bearer,
}
elseif provider == "openai_resp" then
headers = {
"-H",
"Authorization: Bearer " .. bearer,
}
elseif provider == "googleai" then
headers = {}
endpoint = render.template_replace(endpoint, "{{secret}}", bearer)
Expand Down
54 changes: 29 additions & 25 deletions lua/gp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@
local config = require("gp.config")

local M = {
_Name = "Gp", -- plugin name
_state = {}, -- table of state variables
agents = {}, -- table of agents
cmd = {}, -- default command functions
config = {}, -- config variables
hooks = {}, -- user defined command functions
defaults = require("gp.defaults"), -- some useful defaults
_Name = "Gp", -- plugin name
_state = {}, -- table of state variables
agents = {}, -- table of agents
cmd = {}, -- default command functions
config = {}, -- config variables
hooks = {}, -- user defined command functions
defaults = require("gp.defaults"), -- some useful defaults
deprecator = require("gp.deprecator"), -- handle deprecated options
dispatcher = require("gp.dispatcher"), -- handle communication with LLM providers
helpers = require("gp.helper"), -- helper functions
imager = require("gp.imager"), -- image generation module
logger = require("gp.logger"), -- logger module
render = require("gp.render"), -- render module
spinner = require("gp.spinner"), -- spinner module
tasker = require("gp.tasker"), -- tasker module
vault = require("gp.vault"), -- handles secrets
whisper = require("gp.whisper"), -- whisper module
helpers = require("gp.helper"), -- helper functions
imager = require("gp.imager"), -- image generation module
logger = require("gp.logger"), -- logger module
render = require("gp.render"), -- render module
spinner = require("gp.spinner"), -- spinner module
tasker = require("gp.tasker"), -- tasker module
vault = require("gp.vault"), -- handles secrets
whisper = require("gp.whisper"), -- whisper module
}

--------------------------------------------------------------------------------
Expand Down Expand Up @@ -70,7 +70,11 @@ M.setup = function(opts)
M.config.openai_api_key = nil
opts.openai_api_key = nil

M.dispatcher.setup({ providers = opts.providers, curl_params = curl_params })
M.dispatcher.setup({
providers = opts.providers,
curl_params = curl_params,
openai_resp_tools = opts.openai_resp_tools
})
M.config.providers = nil
opts.providers = nil

Expand Down Expand Up @@ -137,11 +141,11 @@ M.setup = function(opts)
elseif not agent.model or not agent.system_prompt then
M.logger.warning(
"Agent "
.. name
.. " is missing model or system_prompt\n"
.. "If you want to disable an agent, use: { name = '"
.. name
.. "', disable = true },"
.. name
.. " is missing model or system_prompt\n"
.. "If you want to disable an agent, use: { name = '"
.. name
.. "', disable = true },"
)
M.agents[name] = nil
end
Expand Down Expand Up @@ -896,7 +900,8 @@ M.cmd.ChatLast = function(params)
end
end
end
buf = win_found and buf or M.open_buf(last, M.resolve_buf_target(params), toggle and M._toggle_kind.chat or nil, toggle)
buf = win_found and buf or
M.open_buf(last, M.resolve_buf_target(params), toggle and M._toggle_kind.chat or nil, toggle)
-- if there is a selection, paste it
if params.range == 2 then
M.render.append_selection(params, cbuf, buf, M.config.template_selection)
Expand Down Expand Up @@ -1281,7 +1286,7 @@ M.cmd.ChatFinder = function()
local command_buf, command_win, command_close, command_resize = M.render.popup(
nil,
"Search: <Tab>/<Shift+Tab>|navigate <Esc>|picker <C-c>|exit "
.. "<Enter>/<C-f>/<C-x>/<C-v>/<C-t>/<C-g>t|open/float/split/vsplit/tab/toggle",
.. "<Enter>/<C-f>/<C-x>/<C-v>/<C-t>/<C-g>t|open/float/split/vsplit/tab/toggle",
function(w, h)
return w - left - right, 1, h - bottom, left
end,
Expand Down Expand Up @@ -1480,7 +1485,6 @@ M.cmd.ChatFinder = function()
local target = M.resolve_buf_target(M.config.toggle_target)
open_chat(target, true)
end)

-- tab in command window will cycle through lines in picker window
M.helpers.set_keymap({ command_buf, picker_buf }, { "i", "n" }, "<tab>", function()
local index = vim.api.nvim_win_get_cursor(picker_win)[1]
Expand Down Expand Up @@ -1947,7 +1951,7 @@ M.Prompt = function(params, target, agent, template, prompt, whisper, callback)
if target == M.Target.rewrite then
-- delete selection
if not (start_line - 1 == 0 and end_line - 1 == 0 and vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] == "") then
vim.api.nvim_buf_set_lines(buf, start_line - 1, end_line - 1, false, {})
vim.api.nvim_buf_set_lines(buf, start_line - 1, end_line - 1, false, {})
end
-- prepare handler
handler = M.dispatcher.create_handler(buf, win, start_line - 1, true, prefix, cursor)
Expand Down
1 change: 0 additions & 1 deletion lua/gp/render.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ local M = {}
---@return string # returns rendered template with specified key replaced by value
M.template_replace = function(template, key, value)
value = value or ""

if type(value) == "table" then
value = table.concat(value, "\n")
end
Expand Down
25 changes: 13 additions & 12 deletions lua/gp/vault.lua
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,26 @@ V.resolve_secret = function(name, secret, callback)
if code == 0 then
local content = stdout_data:match("^%s*(.-)%s*$")
if not string.match(content, "%S") then
logger.warning("vault resolver got empty response for " .. name .. " secret command " .. vim.inspect(secret))
logger.warning("vault resolver got empty response for " ..
name .. " secret command " .. vim.inspect(secret))
return
end
secrets[name] = content
post_process()
else
logger.warning(
"vault resolver for "
.. name
.. "secret command "
.. vim.inspect(secret)
.. " failed:\ncode: "
.. code
.. ", signal: "
.. signal
.. "\nstdout: "
.. stdout_data
.. "\nstderr: "
.. stderr_data
.. name
.. "secret command "
.. vim.inspect(secret)
.. " failed:\ncode: "
.. code
.. ", signal: "
.. signal
.. "\nstdout: "
.. stdout_data
.. "\nstderr: "
.. stderr_data
)
end
end)
Expand Down