diff options
| author | Wayne-Cole <77279425+Wacky404@users.noreply.github.com> | 2026-06-07 17:45:22 -0500 |
|---|---|---|
| committer | Wayne-Cole <77279425+Wacky404@users.noreply.github.com> | 2026-06-07 17:45:22 -0500 |
| commit | 53c60a9213f4e899bc411373ea2f0c20cf716826 (patch) | |
| tree | 63001d83197db89f61890467740db940645fb2ca /lua/99/providers.lua | |
| parent | 4d229141546290746c82ac90f5afc2786865b5f3 (diff) | |
| download | a4-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.lua | 498 |
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, } |
