scripts/list-inference-choices

#!/usr/bin/env luajit
-- list-inference-choices
-- Print the inference servers and embedding models this project knows about,
-- one per line, so the interactive menu can offer them as a pick-list instead of
-- a box to type into.
--
-- General description (for a CEO): the pipeline needs to be told which machine
-- to ask and which model to ask it for. Those are exact strings -- "qwen3-
-- embedding:4b", not "qwen3 embedding 4b" -- and a person typing one from memory
-- will eventually get it wrong, then wait for a stage to fail before finding
-- out. This tool reads the real list out of the configuration file so the menu
-- can show it as a list of choices. You cannot mistype something you pick.
--
-- Why a separate tool rather than a few lines inside run.sh: the list has one
-- source of truth (config.lua's inference_servers), and anything that needs it
-- -- the menu today, a validator or a status page tomorrow -- should read it
-- from the same place rather than each growing its own copy of the parsing.
--
-- Usage:
-- scripts/list-inference-choices [DIR] --servers -> one server name per line
-- scripts/list-inference-choices [DIR] --models -> one model name per line
-- DIR defaults to the hard-coded project root; pass it (as run.sh does) to be safe.
--
-- Exits non-zero with a message on stderr when the config has no servers, rather
-- than printing nothing: an empty list and a broken config look identical to a
-- caller reading stdout, and the caller would render an empty menu section with
-- no idea why.

-- {{{ setup_dir_path(provided)
local function setup_dir_path(provided)
if provided and provided ~= "" then return provided end
return "/mnt/mtwo/programming/ai-stuff/neocities-modernization"
end
-- }}}

-- {{{ parse_args(args)
-- First bare (non "--") argument is DIR; --servers / --models choose the list.
local function parse_args(args)
local dir, what = nil, nil
for _, a in ipairs(args or {}) do
if a == "--servers" then
what = "servers"
elseif a == "--models" then
what = "models"
elseif a:sub(1, 1) ~= "-" then
if not dir then dir = a end
end
end
return dir, what
end
-- }}}

local provided_dir, what = parse_args(arg)
local DIR = setup_dir_path(provided_dir)

if not what then
io.stderr:write("Usage: list-inference-choices [DIR] --servers|--models\n")
os.exit(2)
end

-- config.lua is a static return {...} table, so dofile reads it directly --
-- no config-loader needed, and no chance of this tool disagreeing with the file.
local ok, cfg = pcall(dofile, DIR .. "/config.lua")
if not ok or type(cfg) ~= "table" then
io.stderr:write("list-inference-choices: cannot read " .. DIR .. "/config.lua\n")
os.exit(1)
end

local servers = cfg.inference_servers
if type(servers) ~= "table" or #servers == 0 then
io.stderr:write("list-inference-choices: config.lua has no inference_servers entries\n")
os.exit(1)
end

-- {{{ collect_servers()
-- Just the names, in the order config.lua lists them. Order is preserved rather
-- than sorted: the file's order is the author's ordering, and a menu that
-- reorders someone's list is quietly editing their intent.
local function collect_servers()
local out = {}
for _, s in ipairs(servers) do
if type(s) == "table" and s.name and s.name ~= "" then
out[#out + 1] = s.name
end
end
return out
end
-- }}}

-- {{{ collect_models()
-- Every model any configured server can serve: each server's default model,
-- plus its available_models entries. An available_models entry is either a
-- plain string or a table carrying its own model_path and prompt prefix (see
-- config.lua's "local" server), so both shapes are read.
--
-- Deliberately NOT filtered by what has embeddings on disk. Stage 6 exists to
-- CREATE those embeddings, so a model with none is a legitimate choice there;
-- and for the stages that only read caches, run.sh's own
-- require_embeddings_for_model already refuses a model with nothing behind it
-- and prints what is available. One check, in the place that knows which stage
-- is running, beats two lists that can disagree.
local function collect_models()
local seen, out = {}, {}
local function add(name)
if type(name) == "string" and name ~= "" and not seen[name] then
seen[name] = true
out[#out + 1] = name
end
end
for _, s in ipairs(servers) do
if type(s) == "table" then
add(s.model)
for _, m in ipairs(s.available_models or {}) do
if type(m) == "string" then
add(m)
elseif type(m) == "table" then
add(m.model)
end
end
end
end
return out
end
-- }}}

local list = (what == "servers") and collect_servers() or collect_models()
if #list == 0 then
io.stderr:write("list-inference-choices: no " .. what .. " found in config.lua\n")
os.exit(1)
end
for _, name in ipairs(list) do
io.write(name, "\n")
end