cmp-ai icon indicating copy to clipboard operation
cmp-ai copied to clipboard

Adding a new backend (VeniceAI) - Content-Type Header Issue

Open plague-doctor opened this issue 9 months ago • 7 comments

Issue Description: I am working on adding a new backend (VeniceAI), but I am encountering an issue when trying to use it. The error message 'Content-Type' must be 'application/json' indicates that the request is not being properly formatted as JSON. Despite ensuring that the Content-Type header is correctly added in requests.lua, the issue persists.

Steps to Reproduce:

  1. Attempt to use the VeniceAI backend in your project.
  2. Observe the error message 'Content-Type' must be 'application/json'.

Expected Behavior: The request should be formatted as JSON with the Content-Type header set to application/json.

Actual Behavior: The Content-Type header is set to application/json, but the issue persists.

Additional Information:

  • The code snippet provided is for Lua and is intended to be used with the Neovim environment.
  • The error message suggests that the issue might be related to how the headers are being handled or the request is being constructed.

Code Snippet for Reference:

local requests = require('cmp_ai.requests')

VeniceAI = requests:new(nil)
BASE_URL = 'https://api.venice.ai/api/v1/chat/completions'

-- Function to stringify a table
function stringify_table(tbl)
  local str = ""
  for k, v in pairs(tbl) do
    if type(k) == "string" then
      k = '"' .. k .. '"'
    end
    if type(v) == "table" then
      v = stringify_table(v)
    end
    if type(v) == "string" then
      v = '"' .. v .. '"'
    end
    str = str .. k .. " = " .. v .. ",\n"
  end
  return "{\n" .. str .. "}"
end

-- Function to initialize VeniceAI
function VeniceAI:new(o, params)
  o = o or {}
  setmetatable(o, self)
  self.__index = self
  self.params = vim.tbl_deep_extend('keep', params or {}, {
    model = 'deepseek-coder-v2-lite',
    temperature = 0.1,
    n = 1,
  })

  self.api_key = os.getenv('OPENAI_API_KEY')
  if not self.api_key then
    vim.schedule(function()
      vim.notify('OPENAI_API_KEY environment variable not set', vim.log.levels.ERROR)
    end)
    self.api_key = 'NO_KEY'
  end
  self.headers = {
    'Authorization: Bearer ' .. self.api_key,
  }
  return o
end

-- Function to complete code
function VeniceAI:complete(lines_before, lines_after, cb)
  if not self.api_key then
    vim.schedule(function()
      vim.notify('OPENAI_API_KEY environment variable not set', vim.log.levels.ERROR)
    end)
    return
  end
  local data = {
    messages = {
      {
        role = 'system',
        content = [=[You are a coding companion.
You need to suggest code for the language ]=] .. vim.o.filetype .. [=[
Given some code prefix and suffix for context, output code which should follow the prefix code.
You should only output valid code in the language ]=] .. vim.o.filetype .. [=[
. to clearly define a code block, including white space, we will wrap the code block
with tags.
Make sure to respect the white space and indentation rules of the language.
Do not output anything in plain language, make sure you only use the relevant programming language verbatim.
For example, consider the following request:
<begin_code_prefix>def print_hello():<end_code_prefix><begin_code_suffix>\n    return<end_code_suffix><begin_code_middle>
Your answer should be:

    print("Hello")<end_code_middle>
]=],
      },
      {
        role = 'user',
        content = '<begin_code_prefix>' ..
        lines_before ..
        '<end_code_prefix>' .. '<begin_code_suffix>' .. lines_after .. '<end_code_suffix><begin_code_middle>',
      },
    },
  }
  data = vim.tbl_deep_extend('keep', data, self.params)
  self:Get(BASE_URL, self.headers, data, function(answer)
    vim.notify(stringify_table(answer))   --  <---------------------- this is where the error is thrown.
    local new_data = {}
    if answer.choices then
      for _, response in ipairs(answer.choices) do
        local entry = response.message.content:gsub('<end_code_middle>', '')
        entry = entry:gsub('```', '')
        table.insert(new_data, entry)
      end
    end
    cb(new_data)
  end)
end

function VeniceAI:test()
  self:complete('def factorial(n)\n    if', '    return ans\n', function(data)
    dump(data)
  end)
end

return VeniceAI

Any idea what I am doing wrong?

plague-doctor avatar May 20 '25 02:05 plague-doctor

Take a look here You can change the file name created, where your data is stored, and check if the json data is encoded correctly. In any case, I would not use your stringify function. Try vim.fn.json_encode

tzachar avatar May 20 '25 06:05 tzachar

Thank you very much for pointers. (Don't worry about my crazy function stringify, that was just a workaround.)

I have been able to create file with a payload:

{
  "messages": [
    {
      "role": "system",
      "content": "You are a coding companion.\nYou need to suggest code for the language lua. Given some code prefix and suffix for context, output code which should follow the prefix code.\nYou should only output valid code in the language lua to clearly define a code block, including white space, we will wrap the code block\nwith tags.\nMake sure to respect the white space and indentation rules of the language.\nDo not output anything in plain language, make sure you only use the relevant programming language verbatim.\nFor example, consider the following request:\n<begin_code_prefix>def print_hello():<end_code_prefix><begin_code_suffix>\\n    return<end_code_suffix><begin_code_middle>\nYour answer should be:\n\n    print(\"Hello\")<end_code_middle>\n"
    },
    {
      "role": "user",
      "content": "<begin_code_prefix>      local cmp_ai = require(\"cmp_ai.config\")\n\n      cmp_ai:setup({\n        max_lines = 10,\n        max_timeout_seconds = 60,\n        provider = \"VeniceAI\",\n        provider_options = {\n          model = \"qwen-2.5-coder-32b\",\n        },\n        notify = true,\n        not<end_code_prefix><begin_code_suffix>\n        notify_callback = function(msg)\n          vim.notify(msg)\n        end,\n        run_on_every_keystroke = true,\n        -- ignored_file_types = {\n          -- default is not to ignore\n          -- uncomment to ignore in lua:\n          -- lua = true\n        -- },<end_code_suffix><begin_code_middle>"
    }
  ],
  "temperature": 0.1,
  "venice_parameters": {
    "disable_thinking": true
  },
  "model": "qwen3-4b",
  "n": 1
}

and I was able to check the parameters passed to the curl command:

["https://api.venice.ai/api/v1/chat/completions", "-d", "@/home/plague/plague.json", "--max-time", 60, "-H", "'Authorization: Bearer fv7xxxxxxxFkW'", "-H", "'Content-Type: application/json'"]

so far, so good. However I still get {"error": "'Content-Type' must be 'application/json'"} as the answer... Well, at least the plugin claims that, as I get the proper response when I use the same curl command:

curl https://api.venice.ai/api/v1/chat/completions -d @/home/plague/plague.json -H 'Authorization: Bearer fv7xxxxxxFkW' -H 'Content-Type: application/json':

{
  "id": "chatcmpl-790fa74bd917",
  "object": "chat.completion",
  "created": 1747783807,
  "model": "qwen3-4b",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "reasoning_content": null,
        "content": "      end\n\n      local cmp_ai = require(\"cmp_ai.config\")\n\n      cmp_ai:setup({\n        max_lines = 10,\n        max_timeout_seconds = 60,\n        provider = \"VeniceAI\",\n        provider_options = {\n          model = \"qwen3-4b\",\n          sd\n        },\n        notify = true,\n        notify_callback = function(msg)\n          vim.notify(msg)\n        end,\n        run_on_every_keystroke = true,\n        -- ignored_file_types = {\n          -- default is not to ignore\n          -- uncomment to ignore in lua:",
        "tool_calls": []
      },
      "logprobs": null,
      "finish_reason": "stop",
      "stop_reason": null
    }
  ],
  "usage": {
    "prompt_tokens": 445,
    "total_tokens": 459,
    "completion_tokens": 14,
    "prompt_tokens_details": null
  },
  "prompt_logprobs": null,
  "venice_parameters": {
    "include_venice_system_prompt": true,
    "web_search_citations": []
  }
}

Any ideas?

plague-doctor avatar May 20 '25 23:05 plague-doctor

Can you tell where the error is generated exactly?

strike-ntzachar avatar May 21 '25 13:05 strike-ntzachar

I guess this is the closest spot I can pinpoint the error:

  job
    :new({
      command = 'curl',
      args = args,
      on_exit = vim.schedule_wrap(function(response, exit_code)
        os.remove(tmpfname)
        if exit_code ~= 0 then
          if conf:get('log_errors') then
            vim.notify('An Error Occurred ...', vim.log.levels.ERROR)
          end
          cb({ { error = 'ERROR: API Error' } })
        end

        local result = table.concat(response:result(), '\n')
        local json = self:json_decode(result)
        vim.notify(vim.fn.json_encode(result))                    <---------- This is the spot
        if type(self.params.raw_response_cb) == 'function' then
          self.params.raw_response_cb(json)
        end
        if json == nil then
          cb({ { error = 'No Response.' } })
        else
          cb(json)
        end
      end),
    })
    :start()

It throws:

"{\"error\":\"'Content-Type' must be 'application/json'\"}"
my backend veniceai.lua for a reference:
local requests = require('cmp_ai.requests')

VeniceAI = requests:new(nil)
BASE_URL = 'https://api.venice.ai/api/v1/chat/completions'

function VeniceAI:new(o)
  o = o or {}
  setmetatable(o, self)
  self.__index = self
  self.params = vim.tbl_deep_extend('keep', o or {}, {
    temperature = 0.1,
    n = 1,
  })

  self.api_key = os.getenv('OPENAI_API_KEY')
  if not self.api_key then
    vim.schedule(function()
      vim.notify('OPENAI_API_KEY environment variable not set', vim.log.levels.ERROR)
    end)
    self.api_key = 'NO_KEY'
  end
  self.headers = {
    'Authorization: Bearer ' .. self.api_key,
  }
  return o
end

function VeniceAI:complete(lines_before, lines_after, cb)
  if not self.api_key then
    vim.schedule(function()
      vim.notify('OPENAI_API_KEY environment variable not set', vim.log.levels.ERROR)
    end)
    return
  end
  local data = {
    messages = {
      {
        role = 'system',
        content = [=[You are a coding companion.
You need to suggest code for the language ]=] .. vim.o.filetype .. [=[
. Given some code prefix and suffix for context, output code which should follow the prefix code.
You should only output valid code in the language ]=] .. vim.o.filetype .. [=[
 to clearly define a code block, including white space, we will wrap the code block
with tags.
Make sure to respect the white space and indentation rules of the language.
Do not output anything in plain language, make sure you only use the relevant programming language verbatim.
For example, consider the following request:
<begin_code_prefix>def print_hello():<end_code_prefix><begin_code_suffix>\n    return<end_code_suffix><begin_code_middle>
Your answer should be:

    print("Hello")<end_code_middle>
]=],
      },
      {
        role = 'user',
        content = '<begin_code_prefix>' .. lines_before .. '<end_code_prefix>' .. '<begin_code_suffix>' .. lines_after .. '<end_code_suffix><begin_code_middle>',
      },
    },
  }
  data = vim.tbl_deep_extend('keep', data, self.params)
  self:Get(BASE_URL, self.headers, data, function(answer)
    local new_data = {}
    if answer.choices then
      for _, response in ipairs(answer.choices) do
        local entry = response.message.content:gsub('<end_code_middle>', '')
        entry = entry:gsub('```', '')
        table.insert(new_data, entry)
      end
    end
    cb(new_data)
  end)
end

function VeniceAI:test()
  self:complete('def factorial(n)\n    if', '    return ans\n', function(data)
    dump(data)
  end)
end

return VeniceAI

and a config snippet:

local cmp_ai = require("cmp_ai.config")

   cmp_ai:setup({
     max_lines = 50,
     max_timeout_seconds = 10,
     provider = "VeniceAI",
     provider_options = {
       model = "qwen3-4b",
       venice_parameters = {
         disable_thinking = true,
         strip_thinking_response = true,
         enable_web_search = "off",
       },
     },
     notify = true,
     notify_callback = function(msg)
       vim.notify(msg)
     end,
     run_on_every_keystroke = true,
   })

plague-doctor avatar May 21 '25 21:05 plague-doctor

I think the problem is with the actual data sent to the server, or the server refuses our headers for some reason. Can you try and replace the use of curl with a script of your own:

#!/bin/bash
echo "$@" > /tmp/cmdline
\curl "$@"

and then send back the content of /tmp/cmdline

tzachar avatar May 22 '25 06:05 tzachar

Sure. But not much different to my previous discovery...

the script:

#!/bin/bash
echo "$@" > /home/plague/test.txt
cat /home/plague/plague.json | jq >>  /home/plague/test.txt
curl "$@"

The content is:

https://api.venice.ai/api/v1/chat/completions -d @/home/plague/plague.json --max-time 10 -H 'Authorization: Bearer fv7xxxxxxFkW' -H 'Content-Type: application/json'

{
  "messages": [
    {
      "role": "system",
      "content": "You are a coding companion.\nYou need to suggest code for the language lua. Given some code prefix and suffix for context, output code which should follow the prefix code.\nYou should only output valid code in the language lua to clearly define a code block, including white space, we will wrap the code block\nwith tags.\nMake sure to respect the white space and indentation rules of the language.\nDo not output anything in plain language, make sure you only use the relevant programming language verbatim.\nFor example, consider the following request:\n<begin_code_prefix>def print_hello():<end_code_prefix><begin_code_suffix>\\n    return<end_code_suffix><begin_code_middle>\nYour answer should be:\n\n    print(\"Hello\")<end_code_middle>\n"
    },
    {
      "role": "user",
      "content": "<begin_code_prefix>--[[\n\n    ==========================================================================\n    ==                          PLUGIN MANIFESTS                            ==\n    ==========================================================================\n\nhrsh7th/nvim-cmp             :: A completion plugin for neovim coded in Lua\nhrsh7th/cmp-buffer           :: nvim-cmp source for buffer words\nhrsh7th/cmp-path             :: nvim-cmp source for path\nhrsh7th/cmp-cmdline          :: nvim-cmp source for vim's cmdline\ndmitmel/cmp-cmdline-history  :: nvim-cmp source command-line or search histories\nhrsh7th/cmp-nvim-lsp         :: nvim-cmp source for neovim builtin LSP client\nhrsh7th/cmp-nvim-lsp-signature-help :: nvim-cmp source for displaying function signatures\nL3MON4D3/LuaSnip             :: Snippet Engine for Neovim written in Lua\nrafamadriz/friendly-snippets :: Set of preconfigured snippets for different languages\nsaadparwaiz1/cmp_luasnip     :: nvim-cmp source from luasnip\nExafunction/codeium.nvim     :: A native neovim extension for Codeium\n]]\n\nreturn {\n  {\n    \"hrsh7th/nvim-cmp\",\n    event = \"InsertEnter\",\n    dependencies = {\n      {\"plague-doctor/cmp-ai\", dependencies = { \"nvim-lua/plenary.nvim\" }},\n      \"hrsh7th/cmp-buffer\",\n      \"hrsh7th/cmp-path\",\n      \"hrsh7th/cmp-cmdline\",\n      \"dmitmel/cmp-cmdline-history\",  jj<end_code_prefix><begin_code_suffix>\n      \"hrsh7th/cmp-nvim-lsp\",\n      \"hrsh7th/cmp-nvim-lsp-signature-help\",\n      {\n        \"L3MON4D3/LuaSnip\",\n        build = \"make install_jsregexp\",\n        config = function()\n          require(\"luasnip.loaders.from_vscode\").lazy_load()\n        end,\n        dependencies = {\n          \"rafamadriz/friendly-snippets\",\n          \"saadparwaiz1/cmp_luasnip\",\n        },\n      },\n      {\n        \"Exafunction/codeium.nvim\",\n        cmd = { \"Codeium\" },\n        build = { \":Codeium Auth\" },\n        opts = {},\n        config = function()\n          require(\"codeium\").setup({\n            enterprise_mode = true,\n            api = {\n              host = \"codeium.movember.com\",\n            },\n          })\n        end,\n },\n    },\n    config = function()\n      local cmp_status_ok, cmp = pcall(require, \"cmp\")\n      if not cmp_status_ok then\n        return\n      end\n\n      local snip_status_ok, luasnip = pcall(require, \"luasnip\")\n      if not snip_status_ok then\n        return\n      end\n\n      require(\"luasnip.loaders.from_vscode\").lazy_load()\n\n      local has_words_before = function()\n        unpack = unpack or table.unpack\n        local line, col = unpack(vim.api.nvim_win_get_cursor(0))\n        return col ~= 0 and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match(\"%s\") == nil\n      end\n\n      local cmp_ai = require(\"cmp_ai.config\")\n<end_code_suffix><begin_code_middle>"
    }
  ],
  "temperature": 0.1,
  "venice_parameters": {
    "enable_web_search": "off",
    "disable_thinking": true,
    "strip_thinking_response": true
  },
  "model": "deepseek-coder-v2-lite",
  "n": 1
}

I really can't see any issue with that. curl from console returns:

{
  "id": "chatcmpl-564b46fc95e43156c39497a2bb9cb2c0",
  "object": "chat.completion",
  "created": 1747953646,
  "model": "deepseek-coder-v2-lite",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "reasoning_content": null,
        "content": " ```lua\n      cmp.setup({\n        snippet = {\n          expand = function(args)\n            luasnip.lsp_expand(args.body)\n          end,\n        },\n        mapping = {\n          [\"<C-b>\"] = cmp.mapping(cmp.mapping.scroll_docs(-4), { \"i\", \"c\" }),\n          [\"<C-f>\"] = cmp.mapping(cmp.mapping.scroll_docs(4), { \"i\", \"c\" }),\n          [\"<C-Space>\"] = cmp.mapping(cmp.mapping.complete(), { \"i\", \"c\" }),\n          [\"<C-y>\"] = cmp.config.disable, -- Specify `cmp.config.disable` if you want to remove the default `<C-y>` mapping.\n          [\"<C-e>\"] = cmp.mapping({\n            i = cmp.mapping.abort(),\n            c = cmp.mapping.close(),\n          }),\n          [\"<CR>\"] = cmp.mapping.confirm({ select = true }),\n          [\"<Tab>\"] = cmp.mapping(function(fallback)\n            if cmp.visible() then\n              cmp.select_next_item()\n            elseif luasnip.expand_or_jumpable() then\n              luasnip.expand_or_jump()\n            elseif has_words_before() then\n              cmp.complete()\n            else\n              fallback()\n            end\n          end, { \"i\", \"s\" }),\n          [\"<S-Tab>\"] = cmp.mapping(function(fallback)\n            if cmp.visible() then\n              cmp.select_prev_item()\n            elseif luasnip.jumpable(-1) then\n              luasnip.jump(-1)\n            else\n              fallback()\n            end\n          end, { \"i\", \"s\" }),\n        },\n        sources = cmp.config.sources({\n          { name = \"nvim_lsp\" },\n          { name = \"luasnip\" },\n          { name = \"buffer\" },\n          { name = \"path\" },\n          { name = \"cmp_luasnip\" },\n          { name = \"cmdline\" },\n          { name = \"cmdline_history\" },\n        }),\n        formatting = {\n          fields = { \"kind\", \"abbr\", \"menu\" },\n          format = function(entry, vim_item)\n            local kind = require(\"lspkind\").cmp_format({ mode = \"symbol_text\", maxwidth = 50 })(entry, vim_item)\n            local strings = vim.split(kind.kind, \"%s\", { trimempty = true })\n            kind.kind = \" \" .. (strings[1] or \"\") .. \" \"\n            kind.menu = \"    (\" .. (strings[2] or \"\") .. \")\"\n\n            return kind\n          end,\n        },\n        experimental = {\n          ghost_text = true,\n        },\n      })\n\n      -- Set configuration for specific filetypes\n      cmp.setup.filetype(\"gitcommit\", {\n        sources = cmp.config.sources({\n          { name = \"cmp_git\" }, -- You can specify the `cmp_git` source if you were installed it.\n        }, {\n          { name = \"buffer\" },\n        }),\n      })\n\n      -- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore).\n      cmp.setup.cmdline({ \"/\", \"?\" }, {\n        mapping = cmp.mapping.preset.cmdline(),\n        sources = {\n          { name = \"buffer\" },\n        },\n      })\n\n      -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).\n      cmp.setup.cmdline(\":\", {\n        mapping = cmp.mapping.preset.cmdline(),\n        sources = cmp.config.sources({\n          { name = \"path\" },\n        }, {\n          { name = \"cmdline\" },\n        }),\n      })\n\n      -- Set up cmp_ai\n      cmp_ai.setup({\n       -- Your cmp_ai configuration here\n      })\n    end,\n  },\n}\n```",
        "tool_calls": []
      },
      "logprobs": null,
      "finish_reason": "stop",
      "stop_reason": null
    }
  ],
  "usage": {
    "prompt_tokens": 1169,
    "total_tokens": 2128,
    "completion_tokens": 959,
    "prompt_tokens_details": null
  },
  "prompt_logprobs": null,
  "venice_parameters": {
    "strip_thinking_response": true,
    "disable_thinking": true,
    "enable_web_search": "off",
    "enable_web_citations": false,
    "include_venice_system_prompt": true,
    "web_search_citations": []
  }
}

plague-doctor avatar May 22 '25 22:05 plague-doctor

No idea. Really at a loss here.

tzachar avatar May 23 '25 12:05 tzachar