summaryrefslogtreecommitdiff
path: root/lua/99/providers.lua
diff options
context:
space:
mode:
authorWayne-Cole <77279425+Wacky404@users.noreply.github.com>2026-06-07 17:45:22 -0500
committerWayne-Cole <77279425+Wacky404@users.noreply.github.com>2026-06-07 17:45:22 -0500
commit53c60a9213f4e899bc411373ea2f0c20cf716826 (patch)
tree63001d83197db89f61890467740db940645fb2ca /lua/99/providers.lua
parent4d229141546290746c82ac90f5afc2786865b5f3 (diff)
downloada4-53c60a9213f4e899bc411373ea2f0c20cf716826.tar.xz
a4-53c60a9213f4e899bc411373ea2f0c20cf716826.zip
feat: huge refactor, still broken requests
Diffstat (limited to 'lua/99/providers.lua')
-rw-r--r--lua/99/providers.lua498
1 files changed, 303 insertions, 195 deletions
diff --git a/lua/99/providers.lua b/lua/99/providers.lua
index 7df0174..8875ca8 100644
--- a/lua/99/providers.lua
+++ b/lua/99/providers.lua
@@ -7,14 +7,107 @@
--- @param fn fun(...: any): nil
--- @return fun(...: any): nil
local function once(fn)
- local called = false
- return function(...)
- if called then
- return
+ local called = false
+ return function(...)
+ if called then
+ return
+ end
+ called = true
+ fn(...)
end
- called = true
- fn(...)
- end
+end
+
+--- @class _A4.OpenAIResponse
+--- @field completion _A4.OpenAIResponse.Completion
+--- @field metadata _A4.OpenAIResponse.MetaData
+--- @field id _A4.OpenAIResponse.Id
+--- @field timings _A4.OpenAIResponse.Timings
+
+--- @class _A4.OpenAIResponse.Completion
+--- @field finishReason string
+--- @field index integer
+--- @field message _A4.OpenAIResponse.Completion.Message
+
+--- @class _A4.OpenAIResponse.Completion.Message
+--- @field role string
+--- @field content string
+
+--- @class _A4.OpenAIResponse.MetaData
+--- @field created integer
+--- @field model string
+--- @field systemFingerPrint string
+--- @field object string
+--- @field usage _A4.OpenAIResponse.MetaData.Usage
+
+--- @class _A4.OpenAIResponse.MetaData.Usage
+--- @field completionTks integer
+--- @field promptTks integer
+--- @field totalTks integer
+--- @field promptTksDetails _A4.OpenAIResponse.MetaData.Usage.PromptTksDetails
+
+--- @class _A4.OpenAIResponse.MetaData.Usage.PromptTksDetails
+--- @field cachedTks integer
+
+--- @class _A4.OpenAIResponse.Id
+--- @field id string
+
+--- @class _A4.OpenAIResponse.Timings
+--- @field cacheN integer
+--- @field promptN integer
+--- @field promptMs number
+--- @field promptPerTkMs number
+--- @field promptPerSec number
+--- @field predictedN integer
+--- @field predictedMs number
+--- @field predictedPerTkMs number
+--- @field predictedPerSec number
+
+--- @param raw table
+--- @return _A4.OpenAIResponse
+local function parse_openai_response(raw)
+ local choice = raw.choices[1]
+
+ --- @type _A4.OpenAIResponse
+ local response = {
+ completion = {
+ finishReason = choice.finish_reason,
+ index = choice.index,
+ message = {
+ role = choice.message.role,
+ content = choice.message.content,
+ },
+ },
+ metadata = {
+ created = raw.created,
+ model = raw.model,
+ systemFingerPrint = raw.system_fingerprint,
+ object = raw.object,
+ usage = {
+ completionTks = raw.usage.completion_tokens,
+ promptTks = raw.usage.prompt_tokens,
+ totalTks = raw.usage.total_tokens,
+ promptTksDetails = {
+ cachedTks = raw.usage.prompt_tokens_details.cached_tokens,
+ },
+ },
+ },
+ id = {
+ id = raw.id,
+ },
+ timings = {
+ cacheN = raw.timings.cache_n,
+ promptN = raw.timings.prompt_n,
+ promptMs = raw.timings.prompt_ms,
+ promptPerTkMs = raw.timings.prompt_per_token_ms,
+ promptPerSec = raw.timings.prompt_per_second,
+ predictedN = raw.timings.predicted_n,
+ predictedMs = raw.timings.predicted_ms,
+ predictedPerTkMs = raw.timings.predicted_per_token_ms,
+ predictedPerSec = raw.timings.predicted_per_second,
+ },
+ }
+
+ return response
end
--- @class _99.Providers.BaseProvider
@@ -25,121 +118,96 @@ local BaseProvider = {}
--- @param callback fun(models: string[]|nil, err: string|nil): nil
function BaseProvider.fetch_models(callback)
- callback(nil, "This provider does not support listing models")
+ callback(nil, "This provider does not support listing models")
end
+--- @param data string
+--- @return string
+function BaseProvider:_clean_response(data)
+ local _data, _ = data:gsub("```[a-z]*\n", ""):gsub(" ```", "")
+ return _data
+end
+
+-- TODO: Rewrite this...
--- @param context _99.Prompt
function BaseProvider:_retrieve_response(context)
- local logger = context.logger:set_area(self:_get_provider_name())
- local tmp = context.tmp_file
- local success, result = pcall(function()
- return vim.fn.readfile(tmp)
- end)
+ local logger = context.logger:set_area(self:_get_provider_name())
- if not success then
- logger:error(
- "retrieve_results: failed to read file",
- "tmp_name",
- tmp,
- "error",
- result
- )
- return false, ""
- end
+ if self._get_provider_name ~= "BareMetalProvider" then
+ local tmp = context.tmp_file
+ local success, result = pcall(function()
+ return vim.fn.readfile(tmp)
+ end)
- local str = table.concat(result, "\n")
- logger:debug("retrieve_results", "results", str)
+ if not success then
+ logger:error(
+ "retrieve_results: failed to read file",
+ "tmp_name",
+ tmp,
+ "error",
+ result
+ )
+ return false, ""
+ end
+
+ local str = table.concat(result, "\n")
+ logger:debug("retrieve_results", "results", str)
- return true, str
+ return true, str
+ else
+ assert(
+ context.req_response,
+ "_retrieve_response: BareMetalProvider has no response"
+ )
+ logger:debug("retrieve_results", "results", context.req_response)
+ return true, context.req_response
+ end
end
+-- TODO: Remember that we are ditching the tmp_file; likely 90%
+-- You can just grab the json data and dump it in logs honestly
+-- files only make sense for agentic flows
--- @param query string
--- @param context _99.Prompt
--- @param observer _99.Providers.Observer
function BaseProvider:make_request(query, context, observer)
- observer.on_start()
+ observer.on_start()
- local logger = context.logger:set_area(self:_get_provider_name())
- logger:debug("make_request", "tmp_file", context.tmp_file)
+ local logger = context.logger:set_area(self:_get_provider_name())
+ logger:debug("make_request", "reason", context.operation)
+ logger:debug("make_request", "endpoint", context.endpoint)
- local once_complete = once(
- --- @param status "success" | "failed" | "cancelled"
- ---@param text string
- function(status, text)
- observer.on_complete(status, text)
- end
- )
+ local once_complete = once(
+ --- @param status "success" | "failed" | "cancelled"
+ --- @param resp string
+ function(status, resp)
+ observer.on_complete(status, resp)
+ end
+ )
- local command = self:_build_command(query, context)
- local extra_args = context._99 and context._99.provider_extra_args or {}
- if #extra_args > 0 then
- vim.list_extend(command, extra_args)
- end
- logger:debug("make_request", "command", command)
+ local command = self:_build_command(query, context)
+ logger:debug("make_request", "command", command)
- local proc = vim.system(
- command,
- {
- text = true,
- stdout = vim.schedule_wrap(function(err, data)
- logger:debug("stdout", "data", data)
- if context:is_cancelled() then
- once_complete("cancelled", "")
- return
- end
- if err and err ~= "" then
- logger:debug("stdout#error", "err", err)
- end
- if not err and data then
- observer.on_stdout(data)
- end
- end),
- stderr = vim.schedule_wrap(function(err, data)
- logger:debug("stderr", "data", data)
- if context:is_cancelled() then
- once_complete("cancelled", "")
- return
- end
- if err and err ~= "" then
- logger:debug("stderr#error", "err", err)
- end
- if not err then
- observer.on_stderr(data)
- end
- end),
- },
- vim.schedule_wrap(function(obj)
- if context:is_cancelled() then
- once_complete("cancelled", "")
- logger:debug("on_complete: request has been cancelled")
- return
- end
- if obj.code ~= 0 then
- local str =
- string.format("process exit code: %d\n%s", obj.code, vim.inspect(obj))
- once_complete("failed", str)
- logger:fatal(
- self:_get_provider_name() .. " make_query failed",
- "obj from results",
- obj
- )
- else
- vim.schedule(function()
- local ok, res = self:_retrieve_response(context)
- if ok then
- once_complete("success", res)
- else
- once_complete(
- "failed",
- "unable to retrieve response from temp file"
- )
- end
+ local proc = vim.system(
+ command,
+ { text = true },
+ vim.schedule_wrap(function(obj)
+ if obj.code ~= 0 then
+ once_complete("failed", obj.stderr)
+ return
+ end
+
+ local data = parse_openai_response(vim.json.decode(obj.stdout))
+ if self._get_provider_name == "BareMetalProvider" then
+ data.completion.message.content =
+ self:_clean_response(data.completion.message.content)
+ context.req_response = data.completion.message.content
+ end
+ once_complete("success", data.completion.message.content)
end)
- end
- end)
- )
+ )
- context:_set_process(proc)
+ context:_set_process(proc)
end
--- @class OpenCodeProvider : _99.Providers.BaseProvider
@@ -149,38 +217,38 @@ local OpenCodeProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function OpenCodeProvider._build_command(_, query, context)
- return {
- "opencode",
- "run",
- "--agent",
- "build",
- "-m",
- context.model,
- query,
- }
+ return {
+ "opencode",
+ "run",
+ "--agent",
+ "build",
+ "-m",
+ context.model,
+ query,
+ }
end
--- @return string
function OpenCodeProvider._get_provider_name()
- return "OpenCodeProvider"
+ return "OpenCodeProvider"
end
--- @return string
function OpenCodeProvider._get_default_model()
- return "opencode/claude-sonnet-4-5"
+ return "opencode/claude-sonnet-4-5"
end
function OpenCodeProvider.fetch_models(callback)
- vim.system({ "opencode", "models" }, { text = true }, function(obj)
- vim.schedule(function()
- if obj.code ~= 0 then
- callback(nil, "Failed to fetch models from opencode")
- return
- end
- local models = vim.split(obj.stdout, "\n", { trimempty = true })
- callback(models, nil)
+ vim.system({ "opencode", "models" }, { text = true }, function(obj)
+ vim.schedule(function()
+ if obj.code ~= 0 then
+ callback(nil, "Failed to fetch models from opencode")
+ return
+ end
+ local models = vim.split(obj.stdout, "\n", { trimempty = true })
+ callback(models, nil)
+ end)
end)
- end)
end
--- @class ClaudeCodeProvider : _99.Providers.BaseProvider
@@ -190,24 +258,24 @@ local ClaudeCodeProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function ClaudeCodeProvider._build_command(_, query, context)
- return {
- "claude",
- "--dangerously-skip-permissions",
- "--model",
- context.model,
- "--print",
- query,
- }
+ return {
+ "claude",
+ "--dangerously-skip-permissions",
+ "--model",
+ context.model,
+ "--print",
+ query,
+ }
end
--- @return string
function ClaudeCodeProvider._get_provider_name()
- return "ClaudeCodeProvider"
+ return "ClaudeCodeProvider"
end
--- @return string
function ClaudeCodeProvider._get_default_model()
- return "claude-sonnet-4-5"
+ return "claude-sonnet-4-5"
end
-- TODO: the claude CLI has no way to list available models.
@@ -216,16 +284,16 @@ end
-- Until Anthropic adds a CLI command for this, we have to hardcode the list here.
-- See https://github.com/anthropics/claude-code/issues/12612
function ClaudeCodeProvider.fetch_models(callback)
- callback({
- "claude-opus-4-6",
- "claude-sonnet-4-5",
- "claude-haiku-4-5",
- "claude-opus-4-5",
- "claude-opus-4-1",
- "claude-sonnet-4-0",
- "claude-opus-4-0",
- "claude-3-7-sonnet-latest",
- }, nil)
+ callback({
+ "claude-opus-4-6",
+ "claude-sonnet-4-5",
+ "claude-haiku-4-5",
+ "claude-opus-4-5",
+ "claude-opus-4-1",
+ "claude-sonnet-4-0",
+ "claude-opus-4-0",
+ "claude-3-7-sonnet-latest",
+ }, nil)
end
--- @class CursorAgentProvider : _99.Providers.BaseProvider
@@ -235,38 +303,40 @@ local CursorAgentProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function CursorAgentProvider._build_command(_, query, context)
- return { "cursor-agent", "--model", context.model, "--print", query }
+ return { "cursor-agent", "--model", context.model, "--print", query }
end
--- @return string
function CursorAgentProvider._get_provider_name()
- return "CursorAgentProvider"
+ return "CursorAgentProvider"
end
--- @return string
function CursorAgentProvider._get_default_model()
- return "sonnet-4.5"
+ return "sonnet-4.5"
end
function CursorAgentProvider.fetch_models(callback)
- vim.system({ "cursor-agent", "models" }, { text = true }, function(obj)
- vim.schedule(function()
- if obj.code ~= 0 then
- callback(nil, "Failed to fetch models from cursor-agent")
- return
- end
- local models = {}
- for _, line in ipairs(vim.split(obj.stdout, "\n", { trimempty = true })) do
- -- `cursor-agent models` outputs lines like "model-id - description",
- -- so we grab everything before the first " - " separator
- local id = line:match("^(%S+)%s+%-")
- if id then
- table.insert(models, id)
- end
- end
- callback(models, nil)
+ vim.system({ "cursor-agent", "models" }, { text = true }, function(obj)
+ vim.schedule(function()
+ if obj.code ~= 0 then
+ callback(nil, "Failed to fetch models from cursor-agent")
+ return
+ end
+ local models = {}
+ for _, line in
+ ipairs(vim.split(obj.stdout, "\n", { trimempty = true }))
+ do
+ -- `cursor-agent models` outputs lines like "model-id - description",
+ -- so we grab everything before the first " - " separator
+ local id = line:match("^(%S+)%s+%-")
+ if id then
+ table.insert(models, id)
+ end
+ end
+ callback(models, nil)
+ end)
end)
- end)
end
--- @class KiroProvider : _99.Providers.BaseProvider
@@ -276,25 +346,25 @@ local KiroProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function KiroProvider._build_command(_, query, context)
- return {
- "kiro-cli",
- "chat",
- "--no-interactive",
- "--model",
- context.model,
- "--trust-all-tools",
- query,
- }
+ return {
+ "kiro-cli",
+ "chat",
+ "--no-interactive",
+ "--model",
+ context.model,
+ "--trust-all-tools",
+ query,
+ }
end
--- @return string
function KiroProvider._get_provider_name()
- return "KiroProvider"
+ return "KiroProvider"
end
--- @return string
function KiroProvider._get_default_model()
- return "claude-sonnet-4.5"
+ return "claude-sonnet-4.5"
end
--- @class GeminiCLIProvider : _99.Providers.BaseProvider
@@ -304,36 +374,74 @@ local GeminiCLIProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function GeminiCLIProvider._build_command(_, query, context)
- return {
- "gemini",
- "--approval-mode",
- -- Allow writing to temp files by default. See:
- -- https://geminicli.com/docs/core/policy-engine/#default-policies
- "auto_edit",
- "--model",
- context.model,
- "--prompt",
- query,
- }
+ return {
+ "gemini",
+ "--approval-mode",
+ -- Allow writing to temp files by default. See:
+ -- https://geminicli.com/docs/core/policy-engine/#default-policies
+ "auto_edit",
+ "--model",
+ context.model,
+ "--prompt",
+ query,
+ }
end
--- @return string
function GeminiCLIProvider._get_provider_name()
- return "GeminiCLIProvider"
+ return "GeminiCLIProvider"
end
--- @return string
function GeminiCLIProvider._get_default_model()
- -- Default to auto-routing between pro and flash. See:
- -- https://geminicli.com/docs/cli/model/
- return "auto"
+ -- Default to auto-routing between pro and flash. See:
+ -- https://geminicli.com/docs/cli/model/
+ return "auto"
+end
+
+--- @class BareMetalProvider : _99.Providers.BaseProvider
+--- @field _clean_response fun(self: _99.Providers.BaseProvider, data : string): string
+local BareMetalProvider = setmetatable({}, { __index = BaseProvider })
+
+--- @param query string
+--- @param context _99.Prompt
+--- @return string[]
+function BareMetalProvider._build_command(_, query, context)
+ return {
+ "curl",
+ "-s",
+ context.endpoint,
+ "-H",
+ '"Content-Type: applications/json"',
+ "-d",
+ '"{"messages":[{"role":"system","content":"You are a strict code completion backend for Neovim. Your input will be a code snippet, function signature, or a comment requesting code. CRITICAL DIRECTIONS: 1. Output ONLY valid, executable programming code. 2. Do NOT wrap your response in markdown code blocks. 3. Do NOT include any conversational filler, explanations, greetings, or sign-offs. 4. If you cannot fulfill the request, output nothing or a code comment explaining why."},{"role":"user","content":"'
+ .. query
+ .. '"}],"temperature":0.0,"stream":false}"',
+ }
+end
+
+--- @return string
+function BareMetalProvider._get_provider_name()
+ return "BareMetalProvider"
+end
+
+--- @return string
+function BareMetalProvider._get_default_model()
+ return "Qwen2.5-Coder-3B-Instruct"
+end
+
+function BareMetalProvider.fetch_models(callback)
+ callback({
+ "Qwen2.5-Coder-3B-Instruct",
+ }, nil)
end
return {
- BaseProvider = BaseProvider,
- OpenCodeProvider = OpenCodeProvider,
- ClaudeCodeProvider = ClaudeCodeProvider,
- CursorAgentProvider = CursorAgentProvider,
- KiroProvider = KiroProvider,
- GeminiCLIProvider = GeminiCLIProvider,
+ BaseProvider = BaseProvider,
+ OpenCodeProvider = OpenCodeProvider,
+ ClaudeCodeProvider = ClaudeCodeProvider,
+ CursorAgentProvider = CursorAgentProvider,
+ KiroProvider = KiroProvider,
+ GeminiCLIProvider = GeminiCLIProvider,
+ BareMetalProvider = BareMetalProvider,
}