broker: blocking POST /v1/chat/completions via ffi/curl + dkjson
Phase 0 implementation per PHASE0.md §6.
M.chat(model_cfg, messages) -> content_string | (nil, errmsg)
Builds the OpenAI-compat JSON body:
{ model, messages, stream: false, temperature: model_cfg.temperature ?? 0.2 }
Sends Content-Type and (optionally) Authorization Bearer pulled from
model_cfg.key_env's process environment. Default timeout 60s; overridable
per-model via model_cfg.timeout_ms.
Error surfaces split:
"transport: ..." curl-side (TCP/TLS/timeout)
"decode: ..." non-JSON response body
"api: ..." OpenAI-style { error: { message } } envelope
"broker.chat: no choices[1].message.content..." shape miss
Tested against four canned mock responses (nc -lN listener feeding
HTTP/1.0 + Connection: close so EOF terminates the body): happy path,
api error envelope, raw-text non-JSON, empty choices[]. The on-wire
request body verified as well: POST path, headers, model/messages/
temperature/stream JSON.
Live test against a real llama.cpp/hossenfelder endpoint deferred per
issue #12 (broker endpoint configuration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+59
-5
@@ -1,15 +1,69 @@
|
||||
-- broker.lua — llama.cpp HTTP client.
|
||||
-- Phase 0: blocking POST via libcurl FFI; SSE streaming wired in Phase 1.
|
||||
-- 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])
|
||||
-- messages: list of { role = ..., content = ... } including system prompt
|
||||
-- Returns: assistant content string on success, (nil, errmsg) on failure.
|
||||
-- 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)
|
||||
error("broker.chat: not implemented (Phase 0 pending)")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user