Model Picker For Agent

This commit is contained in:
2026-08-18 14:49:17 -04:00
parent b0fb23a8f5
commit 1ad9d5781e
18 changed files with 2386 additions and 60 deletions
+2 -1
View File
@@ -23,7 +23,7 @@ const preamble = {
awk: ['awk-engine.js'],
jq: ['jq-engine.js'],
man: ['man-render.js'],
'discord-bot': ['bare-os-discord-commands-guest.cjs'],
'discord-bot': ['agent-models.js', 'bare-os-discord-commands-guest.cjs'],
ls: ['bare-os-lscolors.js', 'ls-colors.js'],
dircolors: ['bare-os-lscolors.js'],
edit: [
@@ -130,6 +130,7 @@ const preamble = {
'agent-helpers.js',
'agent-state.js',
'agent-qvac.js',
'agent-models.js',
'agent-workspace.js',
'agent-skills.js',
'agent-sse-parse.js',
@@ -0,0 +1,164 @@
/**
* Shared QVAC chat catalog + REST /models helpers (preamble for agent and discord-bot).
*/
/** @type {{ id: string, family: string, label: string, tools: boolean, ramGb: number, profile?: string }[]} */
var BARE_AGENT_QVAC_CHAT_MODELS = [
{ id: 'QWEN3_600M_INST_Q4', family: 'qwen3', label: 'Qwen3 0.6B Instruct Q4', tools: true, ramGb: 4, profile: 'lite' },
{ id: 'QWEN3_1_7B_INST_Q4', family: 'qwen3', label: 'Qwen3 1.7B Instruct Q4', tools: true, ramGb: 8, profile: 'recommended' },
{ id: 'QWEN3_4B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Instruct Q4_K_M', tools: true, ramGb: 16, profile: 'strong' },
{ id: 'QWEN3_4B_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Q4_K_M', tools: true, ramGb: 16 },
{ id: 'QWEN3_8B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 8B Instruct Q4_K_M', tools: true, ramGb: 24 },
{ id: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K', family: 'llama', label: 'Llama 3.2 1B tool-calling', tools: true, ramGb: 6, profile: 'tool-tiny' },
{ id: 'LLAMA_3_2_1B_INST_Q4_0', family: 'llama', label: 'Llama 3.2 1B Instruct Q4_0', tools: true, ramGb: 6 },
{ id: 'SMOLLM2_360M_INST_Q8', family: 'smol', label: 'SmolLM2 360M Instruct Q8', tools: false, ramGb: 3 },
{ id: 'GPT_OSS_20B_INST_Q4_K_M', family: 'gpt-oss', label: 'GPT-OSS 20B Instruct Q4_K_M', tools: true, ramGb: 24 },
{ id: 'GEMMA4_2B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 2B multimodal Q4', tools: true, ramGb: 8 },
{ id: 'GEMMA4_2B_MULTIMODAL_Q6_K', family: 'gemma', label: 'Gemma 4 2B multimodal Q6', tools: true, ramGb: 10 },
{ id: 'GEMMA4_4B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 4B multimodal Q4', tools: true, ramGb: 16 },
{ id: 'GEMMA4_31B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 31B multimodal Q4', tools: true, ramGb: 48 },
{ id: 'QWEN3VL_2B_MULTIMODAL_Q4_K', family: 'qwen3', label: 'Qwen3-VL 2B multimodal Q4', tools: true, ramGb: 10 },
{ id: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 2B multimodal Q4', tools: true, ramGb: 10 },
{ id: 'QWEN3_5_4B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 4B multimodal Q4', tools: true, ramGb: 16 },
{ id: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 8B multimodal Q4', tools: true, ramGb: 24 },
{ id: 'QWEN3_5_9B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 9B multimodal Q4', tools: true, ramGb: 28 },
{ id: 'QWEN3_6_27B_MULTIMODAL_Q4_K_XL', family: 'large', label: 'Qwen3.6 27B multimodal Q4', tools: true, ramGb: 48 }
]
/** @type {Record<string, string[]>} */
var BARE_AGENT_REST_MODEL_FALLBACKS = {
groq: [
'llama-3.3-70b-versatile',
'llama-3.1-8b-instant',
'openai/gpt-oss-120b',
'openai/gpt-oss-20b',
'qwen/qwen3-32b',
'moonshotai/kimi-k2-instruct',
'meta-llama/llama-4-scout-17b-16e-instruct',
'meta-llama/llama-4-maverick-17b-128e-instruct',
'groq/compound'
],
xai: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212', 'grok-2-vision-1212'],
openai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'o3'],
custom: []
}
/**
* @param {string} [profileId]
*/
function bareAgentQvacModelForProfile(profileId) {
const id = String(profileId || '').trim().toLowerCase()
for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) {
if (BARE_AGENT_QVAC_CHAT_MODELS[i].profile === id) return BARE_AGENT_QVAC_CHAT_MODELS[i]
}
return BARE_AGENT_QVAC_CHAT_MODELS[1] || BARE_AGENT_QVAC_CHAT_MODELS[0]
}
/**
* @param {string} [modelId]
*/
function bareAgentQvacFindChatModel(modelId) {
const id = String(modelId || '').trim()
for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) {
if (BARE_AGENT_QVAC_CHAT_MODELS[i].id === id) return BARE_AGENT_QVAC_CHAT_MODELS[i]
}
return null
}
/**
* @param {{ id: string, label?: string, family?: string }[]} models
* @param {{ family?: string, query?: string }} [opts]
*/
function bareAgentFilterModelList(models, opts) {
const o = opts && typeof opts === 'object' ? opts : {}
const fam = String(o.family || 'all').trim().toLowerCase()
const q = String(o.query || '').trim().toLowerCase()
const rows = Array.isArray(models) ? models : []
/** @type {{ id: string, label?: string, family?: string }[]} */
const out = []
for (let i = 0; i < rows.length; i++) {
const m = rows[i]
if (!m || !m.id) continue
if (fam && fam !== 'all' && String(m.family || '').toLowerCase() !== fam) continue
if (q) {
const blob = (String(m.id) + ' ' + String(m.label || '')).toLowerCase()
if (blob.indexOf(q) === -1) continue
}
out.push(m)
}
return out
}
/**
* @param {unknown} json
* @returns {{ id: string, label: string, family: string, owned_by: string }[]}
*/
function bareAgentParseOpenAiModels(json) {
const raw =
json && typeof json === 'object' && Array.isArray(/** @type {{ data?: unknown }} */ (json).data)
? /** @type {{ data: unknown[] }} */ (json).data
: Array.isArray(json)
? json
: []
const skip = /embed|whisper|tts|dall-e|davinci|babbage|audio|moderation|realtime|image|sora/i
/** @type {{ id: string, label: string, family: string, owned_by: string }[]} */
const out = []
const seen = Object.create(null)
for (let i = 0; i < raw.length; i++) {
const row = raw[i] && typeof raw[i] === 'object' ? /** @type {Record<string, unknown>} */ (raw[i]) : null
if (!row) continue
const id = String(row.id || row.name || '').trim()
if (!id || seen[id] || skip.test(id)) continue
seen[id] = 1
const owned = String(row.owned_by || row.ownedBy || '')
out.push({
id: id,
label: id,
family: owned || 'api',
owned_by: owned
})
}
out.sort(function (a, b) {
return a.id.localeCompare(b.id)
})
return out
}
/**
* @param {string} [provider]
*/
function bareAgentRestModelsFallback(provider) {
const key = String(provider || 'groq').trim().toLowerCase()
const ids = BARE_AGENT_REST_MODEL_FALLBACKS[key] || BARE_AGENT_REST_MODEL_FALLBACKS.groq
return (ids || []).map(function (id) {
return { id: id, label: id, family: key, owned_by: key }
})
}
/**
* @param {(url: string, init?: object) => Promise<{ ok?: boolean, status?: number, json?: () => Promise<unknown>, text?: () => Promise<string> }>} fetchFn
* @param {{ baseUrl?: string, apiKey?: string }} opts
*/
async function bareAgentFetchRestModels(fetchFn, opts) {
const o = opts && typeof opts === 'object' ? opts : {}
const base = String(o.baseUrl || '').trim().replace(/\/+$/, '')
if (!base) throw new Error('rest_base_url required')
if (typeof fetchFn !== 'function') throw new Error('fetch unavailable')
const url = base + '/models'
/** @type {Record<string, string>} */
const headers = { Accept: 'application/json' }
const key = String(o.apiKey || '').trim()
if (key) headers.Authorization = 'Bearer ' + key
const res = await fetchFn(url, { method: 'GET', headers: headers })
if (!res || res.ok === false) {
const st = res && res.status != null ? String(res.status) : 'fetch_failed'
throw new Error('models_http_' + st)
}
let json
if (typeof res.json === 'function') json = await res.json()
else if (typeof res.text === 'function') json = JSON.parse(await res.text())
else throw new Error('models_unreadable')
const list = bareAgentParseOpenAiModels(json)
if (!list.length) throw new Error('models_empty')
return list
}
+6 -3
View File
@@ -217,7 +217,10 @@ const BARE_AGENT_REST_PROVIDERS = {
models: [
'llama-3.3-70b-versatile',
'llama-3.1-8b-instant',
'openai/gpt-oss-120b'
'openai/gpt-oss-120b',
'openai/gpt-oss-20b',
'qwen/qwen3-32b',
'moonshotai/kimi-k2-instruct'
]
},
xai: {
@@ -225,14 +228,14 @@ const BARE_AGENT_REST_PROVIDERS = {
label: 'xAI (Grok)',
rest_base_url: 'https://api.x.ai/v1',
default_model: 'grok-4',
models: ['grok-4', 'grok-3', 'grok-3-mini']
models: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212']
},
openai: {
id: 'openai',
label: 'OpenAI',
rest_base_url: 'https://api.openai.com/v1',
default_model: 'gpt-4.1',
models: ['gpt-4.1', 'gpt-4.1-mini', 'o4-mini']
models: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'o4-mini']
},
custom: {
id: 'custom',
+43 -6
View File
@@ -1027,10 +1027,47 @@ async function bareAgentSetupRestFieldsPlain(ctx, stdout, config) {
throw new Error('REST API key is required')
}
if (spec.models.length) {
let mlines = '\nSuggested models for ' + spec.label + ':\n'
for (let i = 0; i < spec.models.length; i++) {
mlines += ' ' + String(i + 1) + ') ' + spec.models[i] + '\n'
/** @type {{ id: string }[]} */
let remote = []
const fetchFn =
typeof ctx.httpFetch === 'function'
? ctx.httpFetch.bind(ctx)
: typeof fetch === 'function'
? fetch
: null
if (fetchFn && typeof bareAgentFetchRestModels === 'function') {
try {
remote = await bareAgentFetchRestModels(fetchFn, {
baseUrl: config.rest_base_url,
apiKey: config.rest_api_key
})
bareAgentWriteOut(
ctx,
stdout,
'\nLive models from ' + config.rest_base_url + ' (' + String(remote.length) + '):\n'
)
} catch (err) {
const msg =
err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err)
bareAgentWriteOut(ctx, stdout, '\nCould not list remote models (' + msg + '). Using curated list.\n')
}
}
const suggest = remote.length
? remote.slice(0, 16).map(function (m) {
return m.id
})
: spec.models && spec.models.length
? spec.models
: typeof bareAgentRestModelsFallback === 'function'
? bareAgentRestModelsFallback(spec.id).map(function (m) {
return m.id
})
: []
if (suggest.length) {
let mlines = remote.length ? '' : '\nSuggested models for ' + spec.label + ':\n'
if (!mlines) mlines = ''
for (let i = 0; i < suggest.length; i++) {
mlines += ' ' + String(i + 1) + ') ' + suggest[i] + '\n'
}
bareAgentWriteOut(ctx, stdout, mlines)
}
@@ -1045,8 +1082,8 @@ async function bareAgentSetupRestFieldsPlain(ctx, stdout, config) {
)) || ''
const modelPick = modelRaw.trim()
const modelNum = Number(modelPick)
if (Number.isFinite(modelNum) && spec.models[modelNum - 1]) {
config.model = spec.models[modelNum - 1]
if (Number.isFinite(modelNum) && suggest[modelNum - 1]) {
config.model = suggest[modelNum - 1]
} else if (modelPick) {
config.model = modelPick
} else if (modelDefault) {
+1 -1
View File
@@ -7,7 +7,7 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-grok-port.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-grok-port.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/agent-models.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"perf:baretop": "node ./test/baretop-perf-baseline.test.mjs"
}
}
@@ -0,0 +1,58 @@
import test from 'brittle'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'
const SRC = readFileSync(new URL('../lib/agent-models.js', import.meta.url), 'utf8')
function load() {
const sandbox = { console }
vm.createContext(sandbox)
vm.runInContext(SRC, sandbox, { filename: 'agent-models.js' })
return sandbox
}
test('QVAC catalog covers profiles and extra chat ids', async (t) => {
const s = load()
const ids = s.BARE_AGENT_QVAC_CHAT_MODELS.map((m) => m.id)
t.ok(ids.includes('QWEN3_1_7B_INST_Q4'))
t.ok(ids.includes('QWEN3_8B_INST_Q4_K_M'))
t.ok(ids.includes('LLAMA_TOOL_CALLING_1B_INST_Q4_K'))
t.ok(ids.includes('GEMMA4_2B_MULTIMODAL_Q4_K_M'))
t.is(s.bareAgentQvacModelForProfile('recommended').id, 'QWEN3_1_7B_INST_Q4')
t.is(s.bareAgentQvacModelForProfile('strong').id, 'QWEN3_4B_INST_Q4_K_M')
})
test('parse OpenAI models skips embeddings and sorts', async (t) => {
const s = load()
const list = s.bareAgentParseOpenAiModels({
data: [
{ id: 'llama-3.3-70b-versatile', owned_by: 'meta' },
{ id: 'text-embedding-3-large', owned_by: 'openai' },
{ id: 'whisper-large-v3', owned_by: 'openai' },
{ id: 'gpt-4.1', owned_by: 'openai' }
]
})
t.is(list.length, 2)
t.is(list[0].id, 'gpt-4.1')
t.is(list[1].id, 'llama-3.3-70b-versatile')
})
test('fetch REST models uses /models and falls back on empty', async (t) => {
const s = load()
const live = await s.bareAgentFetchRestModels(
async () => ({
ok: true,
status: 200,
json: async () => ({ data: [{ id: 'grok-4' }, { id: 'grok-3-mini' }] })
}),
{ baseUrl: 'https://api.x.ai/v1', apiKey: 'x' }
)
t.is(live.length, 2)
t.is(live[0].id, 'grok-3-mini')
await t.exception(() =>
s.bareAgentFetchRestModels(async () => ({ ok: false, status: 401 }), {
baseUrl: 'https://api.groq.com/openai/v1'
})
)
t.ok(s.bareAgentRestModelsFallback('groq').some((m) => m.id === 'llama-3.3-70b-versatile'))
})