377 lines
11 KiB
JavaScript
377 lines
11 KiB
JavaScript
/**
|
|
* QVAC helpers for /bin/agent (no SDK import — host bridge via ctx.bareOsQvac*).
|
|
*/
|
|
|
|
/** @typedef {'lite'|'recommended'|'strong'|'tool-tiny'} BareAgentQvacProfileId */
|
|
|
|
/**
|
|
* Native context windows from upstream model cards (not conservative laptop defaults).
|
|
* Qwen3 dense (0.6B/1.7B/4B): https://huggingface.co/Qwen/Qwen3-0.6B — 32,768
|
|
* Llama 3.2 1B (tool-calling finetune base): Meta Llama 3.2 — 128,000
|
|
* Absolute ceiling for overrides / host clamp.
|
|
*/
|
|
const BARE_AGENT_QVAC_CTX_QWEN3 = 32768
|
|
const BARE_AGENT_QVAC_CTX_LLAMA32_1B = 131072
|
|
const BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX = 131072
|
|
|
|
/**
|
|
* @typedef {{
|
|
* id: BareAgentQvacProfileId,
|
|
* label: string,
|
|
* description: string,
|
|
* chatModel: string,
|
|
* tools: boolean,
|
|
* minRamGb: number,
|
|
* minDiskGb: number,
|
|
* approxDownloadGb: number,
|
|
* ctxSize: number
|
|
* }} BareAgentQvacProfile
|
|
*/
|
|
|
|
/** @type {Record<BareAgentQvacProfileId, BareAgentQvacProfile>} */
|
|
const BARE_AGENT_QVAC_PROFILES = {
|
|
lite: {
|
|
id: 'lite',
|
|
label: 'Lite',
|
|
description:
|
|
'Smallest download (Qwen3-0.6B). Model-card context 32k; tools enabled.',
|
|
chatModel: 'QWEN3_600M_INST_Q4',
|
|
tools: true,
|
|
minRamGb: 4,
|
|
minDiskGb: 2,
|
|
approxDownloadGb: 0.5,
|
|
ctxSize: BARE_AGENT_QVAC_CTX_QWEN3
|
|
},
|
|
recommended: {
|
|
id: 'recommended',
|
|
label: 'Recommended',
|
|
description:
|
|
'Best balance for Bare OS agent tool calling (Qwen3-1.7B, model-card context 32k).',
|
|
chatModel: 'QWEN3_1_7B_INST_Q4',
|
|
tools: true,
|
|
minRamGb: 8,
|
|
minDiskGb: 5,
|
|
approxDownloadGb: 2.5,
|
|
ctxSize: BARE_AGENT_QVAC_CTX_QWEN3
|
|
},
|
|
strong: {
|
|
id: 'strong',
|
|
label: 'Strong',
|
|
description:
|
|
'Better reasoning (Qwen3-4B). Model-card context 32k (131k with YaRN not enabled).',
|
|
chatModel: 'QWEN3_4B_INST_Q4_K_M',
|
|
tools: true,
|
|
minRamGb: 16,
|
|
minDiskGb: 8,
|
|
approxDownloadGb: 3.5,
|
|
ctxSize: BARE_AGENT_QVAC_CTX_QWEN3
|
|
},
|
|
'tool-tiny': {
|
|
id: 'tool-tiny',
|
|
label: 'Tool-tiny',
|
|
description:
|
|
'Llama 3.2 1B tool-calling fallback (model-card context 128k).',
|
|
chatModel: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K',
|
|
tools: true,
|
|
minRamGb: 6,
|
|
minDiskGb: 3,
|
|
approxDownloadGb: 1,
|
|
ctxSize: BARE_AGENT_QVAC_CTX_LLAMA32_1B
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Model-card / train context for a QVAC registry id (fallback 32k).
|
|
* @param {string} [modelId]
|
|
* @returns {number}
|
|
*/
|
|
function bareAgentQvacModelCardCtxSize(modelId) {
|
|
const id = String(modelId || '')
|
|
.trim()
|
|
.toUpperCase()
|
|
if (!id) return BARE_AGENT_QVAC_CTX_QWEN3
|
|
if (id.includes('LLAMA') || id.includes('LLAMA_TOOL')) {
|
|
return BARE_AGENT_QVAC_CTX_LLAMA32_1B
|
|
}
|
|
if (id.includes('QWEN3')) return BARE_AGENT_QVAC_CTX_QWEN3
|
|
return BARE_AGENT_QVAC_CTX_QWEN3
|
|
}
|
|
|
|
/** @returns {BareAgentQvacProfile[]} */
|
|
function bareAgentQvacProfileList() {
|
|
return Object.values(BARE_AGENT_QVAC_PROFILES)
|
|
}
|
|
|
|
/**
|
|
* @param {string} [id]
|
|
* @returns {BareAgentQvacProfile}
|
|
*/
|
|
function bareAgentQvacGetProfile(id) {
|
|
const key = String(id || '').trim().toLowerCase()
|
|
return BARE_AGENT_QVAC_PROFILES[key] || BARE_AGENT_QVAC_PROFILES.recommended
|
|
}
|
|
|
|
/**
|
|
* Flatten OpenAI nested tool defs → QVAC flat shape.
|
|
* @param {unknown[]} tools
|
|
* @returns {unknown[]}
|
|
*/
|
|
function bareAgentFlattenToolsForQvac(tools) {
|
|
if (!Array.isArray(tools)) return []
|
|
/** @type {unknown[]} */
|
|
const out = []
|
|
for (const t of tools) {
|
|
if (!t || typeof t !== 'object') continue
|
|
const o = /** @type {Record<string, unknown>} */ (t)
|
|
if (o.function && typeof o.function === 'object') {
|
|
const fn = /** @type {Record<string, unknown>} */ (o.function)
|
|
out.push({
|
|
type: 'function',
|
|
name: typeof fn.name === 'string' ? fn.name : '',
|
|
description: typeof fn.description === 'string' ? fn.description : '',
|
|
parameters:
|
|
fn.parameters && typeof fn.parameters === 'object'
|
|
? fn.parameters
|
|
: { type: 'object', properties: {} }
|
|
})
|
|
continue
|
|
}
|
|
if (typeof o.name === 'string') {
|
|
out.push({
|
|
type: 'function',
|
|
name: o.name,
|
|
description: typeof o.description === 'string' ? o.description : '',
|
|
parameters:
|
|
o.parameters && typeof o.parameters === 'object'
|
|
? o.parameters
|
|
: { type: 'object', properties: {} }
|
|
})
|
|
}
|
|
}
|
|
return out.filter((x) => x && typeof x === 'object' && /** @type {any} */ (x).name)
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @returns {boolean}
|
|
*/
|
|
function bareAgentQvacBridgeAvailable(ctx) {
|
|
if (!ctx || typeof ctx !== 'object') return false
|
|
if (typeof ctx.bareOsQvacAvailable === 'function') {
|
|
try {
|
|
return Boolean(ctx.bareOsQvacAvailable())
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
return typeof ctx.bareOsQvacComplete === 'function'
|
|
}
|
|
|
|
/**
|
|
* Normalize backend from config (qvac | rest).
|
|
* @param {Record<string, unknown>} config
|
|
* @returns {'qvac'|'rest'}
|
|
*/
|
|
function bareAgentResolveBackend(config) {
|
|
const b = String(config?.backend || '').trim().toLowerCase()
|
|
if (b === 'rest' || b === 'openai' || b === 'http') return 'rest'
|
|
if (b === 'qvac') return 'qvac'
|
|
const p = String(config?.provider || '').trim().toLowerCase()
|
|
if (p === 'qvac') return 'qvac'
|
|
if (p === 'groq' || p === 'xai' || p === 'openai' || p === 'custom') return 'rest'
|
|
// Default: qvac when key unset; rest when key present (legacy configs)
|
|
if (config?.rest_api_key && String(config.rest_api_key).trim()) return 'rest'
|
|
return 'qvac'
|
|
}
|
|
|
|
/** @type {readonly string[]} */
|
|
var BARE_AGENT_QVAC_ONLY_KEYS = Object.freeze([
|
|
'qvac_model',
|
|
'qvac_profile',
|
|
'qvac_ctx_size',
|
|
'qvac_device',
|
|
'qvac_main_gpu',
|
|
'qvac_gpu_layers'
|
|
])
|
|
|
|
/** @type {readonly string[]} */
|
|
var BARE_AGENT_REST_ONLY_KEYS = Object.freeze(['rest_base_url', 'rest_api_key'])
|
|
|
|
/**
|
|
* @typedef {{
|
|
* id: string,
|
|
* label: string,
|
|
* rest_base_url: string,
|
|
* default_model: string,
|
|
* models: string[]
|
|
* }} BareAgentRestProvider
|
|
*/
|
|
|
|
/** @type {Record<string, BareAgentRestProvider>} */
|
|
const BARE_AGENT_REST_PROVIDERS = {
|
|
groq: {
|
|
id: 'groq',
|
|
label: 'Groq',
|
|
rest_base_url: 'https://api.groq.com/openai/v1',
|
|
default_model: 'llama-3.3-70b-versatile',
|
|
models: [
|
|
'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'
|
|
]
|
|
},
|
|
xai: {
|
|
id: 'xai',
|
|
label: 'xAI (Grok)',
|
|
rest_base_url: 'https://api.x.ai/v1',
|
|
default_model: 'grok-4',
|
|
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', 'gpt-4.1-nano', 'gpt-4o', 'o4-mini']
|
|
},
|
|
custom: {
|
|
id: 'custom',
|
|
label: 'Custom OpenAI-compatible',
|
|
rest_base_url: '',
|
|
default_model: '',
|
|
models: []
|
|
}
|
|
}
|
|
|
|
/** @returns {BareAgentRestProvider[]} */
|
|
function bareAgentRestProviderList() {
|
|
return [BARE_AGENT_REST_PROVIDERS.groq, BARE_AGENT_REST_PROVIDERS.xai, BARE_AGENT_REST_PROVIDERS.openai, BARE_AGENT_REST_PROVIDERS.custom]
|
|
}
|
|
|
|
/**
|
|
* @param {string} [id]
|
|
* @returns {BareAgentRestProvider}
|
|
*/
|
|
function bareAgentRestGetProvider(id) {
|
|
const key = String(id || '').trim().toLowerCase()
|
|
if (key === 'http' || key === 'rest') return BARE_AGENT_REST_PROVIDERS.custom
|
|
return BARE_AGENT_REST_PROVIDERS[key] || BARE_AGENT_REST_PROVIDERS.groq
|
|
}
|
|
|
|
/**
|
|
* @param {string} [model]
|
|
*/
|
|
function bareAgentIsQvacModelId(model) {
|
|
const m = String(model || '').trim()
|
|
if (!m) return false
|
|
return (
|
|
/^QWEN/i.test(m) ||
|
|
/^LLAMA_TOOL/i.test(m) ||
|
|
/QWEN3/i.test(m) ||
|
|
/LLAMA_TOOL_CALLING/i.test(m)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Drop keys that belong to the other backend so config.json matches the choice.
|
|
* @param {Record<string, unknown>} config
|
|
* @returns {Record<string, unknown>}
|
|
*/
|
|
function bareAgentSanitizeConfigForBackend(config) {
|
|
const out = { ...(config && typeof config === 'object' ? config : {}) }
|
|
const backend = bareAgentResolveBackend(out)
|
|
out.backend = backend
|
|
if (backend === 'rest') {
|
|
for (let i = 0; i < BARE_AGENT_QVAC_ONLY_KEYS.length; i++) {
|
|
delete out[BARE_AGENT_QVAC_ONLY_KEYS[i]]
|
|
}
|
|
const prov = String(out.provider || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
if (!prov || prov === 'qvac') out.provider = 'groq'
|
|
if (bareAgentIsQvacModelId(String(out.model || ''))) {
|
|
const spec = bareAgentRestGetProvider(String(out.provider || 'groq'))
|
|
if (spec.default_model) out.model = spec.default_model
|
|
}
|
|
} else {
|
|
for (let i = 0; i < BARE_AGENT_REST_ONLY_KEYS.length; i++) {
|
|
delete out[BARE_AGENT_REST_ONLY_KEYS[i]]
|
|
}
|
|
out.provider = 'qvac'
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* @param {string} [secret]
|
|
*/
|
|
function bareAgentMaskSecretPreview(secret) {
|
|
const t = String(secret || '')
|
|
if (!t.trim()) return '(not set)'
|
|
if (t.length <= 8) return '********'
|
|
return t.slice(0, 3) + '…' + t.slice(-4)
|
|
}
|
|
|
|
/**
|
|
* Resolve context window for QVAC load from the model card / profile.
|
|
* Legacy undersized `qvac_ctx_size` values (e.g. 4096/8192) are ignored so
|
|
* setup stays automatic; only overrides ≥ the profile default apply.
|
|
* Host may still cap via `BARE_OS_QVAC_MAX_CTX`.
|
|
* @param {Record<string, unknown>} config
|
|
* @param {BareAgentQvacProfile} profile
|
|
*/
|
|
function bareAgentQvacResolveCtxSize(config, profile) {
|
|
const modelId = String(
|
|
(config && (config.qvac_model || config.model)) ||
|
|
(profile && profile.chatModel) ||
|
|
''
|
|
)
|
|
const cardCtx = bareAgentQvacModelCardCtxSize(modelId)
|
|
const profileCtx = Math.max(
|
|
2048,
|
|
Number(profile && profile.ctxSize) || cardCtx
|
|
)
|
|
let ctx = Math.min(
|
|
BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX,
|
|
Math.max(profileCtx, cardCtx)
|
|
)
|
|
const raw = Number(config && config.qvac_ctx_size)
|
|
if (Number.isFinite(raw) && raw >= profileCtx) {
|
|
ctx = Math.min(BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX, Math.floor(raw))
|
|
}
|
|
// Full Bare OS tool list ≈ 4.5k tokens; leave room for system + reply.
|
|
// Tools are always on for agent sessions — keep enough ctx for schemas.
|
|
if (ctx < 8192) ctx = 8192
|
|
return ctx
|
|
}
|
|
|
|
/**
|
|
* Device / main-gpu / layers for QVAC load (config + profile defaults).
|
|
* @param {Record<string, unknown>} config
|
|
* @returns {{ device?: string, mainGpu?: string | number, gpuLayers?: number }}
|
|
*/
|
|
function bareAgentQvacResolveDeviceOpts(config) {
|
|
/** @type {{ device?: string, mainGpu?: string | number, gpuLayers?: number }} */
|
|
const out = {}
|
|
const device = String(config && config.qvac_device ? config.qvac_device : '')
|
|
.trim()
|
|
.toLowerCase()
|
|
if (device === 'cpu' || device === 'gpu') out.device = device
|
|
|
|
const mainRaw = config && config.qvac_main_gpu
|
|
if (mainRaw !== undefined && mainRaw !== null && String(mainRaw).trim() !== '') {
|
|
const s = String(mainRaw).trim().toLowerCase()
|
|
if (s === 'auto' || s === 'dedicated' || s === 'integrated') out.mainGpu = s
|
|
else if (/^\d+$/.test(s)) out.mainGpu = Number.parseInt(s, 10)
|
|
} else {
|
|
out.mainGpu = 'auto'
|
|
}
|
|
|
|
const layers = Number(config && config.qvac_gpu_layers)
|
|
if (Number.isFinite(layers) && layers >= 0) out.gpuLayers = Math.floor(layers)
|
|
return out
|
|
}
|