-- broker.lua — llama.cpp HTTP client. -- Phase 0: blocking POST via ffi/curl + vendored dkjson; the response is read -- to completion and returned as a Lua string. SSE streaming wired in Phase 1. -- See docs/PHASE0.md §6. local curl = require("ffi.curl") local json = require("dkjson") local M = {} local function build_headers(model_cfg) local h = { "Content-Type: application/json" } if model_cfg.key_env then local key = os.getenv(model_cfg.key_env) if key and key ~= "" then h[#h + 1] = "Authorization: Bearer " .. key end end return h end -- Send a /v1/chat/completions request. -- model_cfg : entry from config.models — { endpoint, model, temperature, -- [key_env], [timeout_ms] } -- messages : ordered list of { role = ..., content = ... }, system -- prompt already prepended (context:to_messages handles that). -- Returns: -- assistant_content_string on success -- nil, errmsg on transport / decode / API failure function M.chat(model_cfg, messages) if not (model_cfg and model_cfg.endpoint and model_cfg.model) then return nil, "broker.chat: model_cfg.endpoint and .model are required" end local url = model_cfg.endpoint:gsub("/+$", "") .. "/v1/chat/completions" local body = json.encode({ model = model_cfg.model, messages = messages, stream = false, temperature = model_cfg.temperature or 0.2, }) local headers = build_headers(model_cfg) local timeout_ms = model_cfg.timeout_ms or 60000 local resp, err = curl.post(url, body, headers, timeout_ms) if not resp then return nil, "transport: " .. tostring(err) end local doc, _, derr = json.decode(resp) if not doc then return nil, "decode: " .. tostring(derr) .. " (raw: " .. resp:sub(1, 200) .. ")" end -- OpenAI-style error envelope: { error: { message, type, ... } } if doc.error then local m = (type(doc.error) == "table" and doc.error.message) or tostring(doc.error) return nil, "api: " .. m end local choice = doc.choices and doc.choices[1] local msg = choice and choice.message local content = msg and msg.content if type(content) ~= "string" then return nil, "broker.chat: no choices[1].message.content in response" end return content end return M