Fix Tool Calling
Release rolling / release (push) Successful in 10m8s

This commit is contained in:
2026-08-18 20:44:36 -04:00
parent d3aa66b0cc
commit 9d53253e48
10 changed files with 2697 additions and 444 deletions
@@ -224,6 +224,69 @@ export function bareOsQvacPickVulkanPin(ranked) {
return { index: null, note: '' }
}
const BARE_OS_QVAC_PROP_TYPES = Object.freeze([
'string',
'number',
'integer',
'boolean',
'object',
'array'
])
/**
* QVAC `toolSchema` only allows { type, description?, enum? } per property.
* Extra keys (items, default, nested properties) fail the request and the
* SDK then completes with no tools attached.
* @param {unknown} params
* @returns {{ type: 'object', properties: Record<string, unknown>, required?: string[] }}
*/
export function bareOsQvacSanitizeToolParameters(params) {
const src =
params && typeof params === 'object'
? /** @type {Record<string, unknown>} */ (params)
: {}
const rawProps =
src.properties && typeof src.properties === 'object' && !Array.isArray(src.properties)
? /** @type {Record<string, unknown>} */ (src.properties)
: {}
/** @type {Record<string, unknown>} */
const properties = {}
for (const key of Object.keys(rawProps)) {
const v = rawProps[key]
const prop =
v && typeof v === 'object' && !Array.isArray(v)
? /** @type {Record<string, unknown>} */ (v)
: {}
let type = typeof prop.type === 'string' ? prop.type : 'string'
if (BARE_OS_QVAC_PROP_TYPES.indexOf(type) === -1) type = 'string'
/** @type {Record<string, unknown>} */
const next = { type: type }
if (typeof prop.description === 'string' && prop.description) {
next.description = prop.description
}
if (Array.isArray(prop.enum)) {
const en = prop.enum.filter(
(x) =>
typeof x === 'string' ||
typeof x === 'number' ||
typeof x === 'boolean' ||
x === null
)
if (en.length) next.enum = en
}
properties[key] = next
}
/** @type {{ type: 'object', properties: Record<string, unknown>, required?: string[] }} */
const out = { type: 'object', properties }
if (Array.isArray(src.required)) {
const req = src.required
.map((x) => String(x || ''))
.filter((k) => k && Object.prototype.hasOwnProperty.call(properties, k))
if (req.length) out.required = req
}
return out
}
/**
* Flatten OpenAI nested tool defs to QVAC flat shape.
* @param {unknown[]} tools
@@ -238,30 +301,119 @@ export function bareOsQvacFlattenTools(tools) {
const o = /** @type {Record<string, unknown>} */ (t)
if (o.function && typeof o.function === 'object') {
const fn = /** @type {Record<string, unknown>} */ (o.function)
const name = typeof fn.name === 'string' ? fn.name : ''
if (!name) continue
out.push({
type: 'function',
name: typeof fn.name === 'string' ? fn.name : '',
name,
description: typeof fn.description === 'string' ? fn.description : '',
parameters:
fn.parameters && typeof fn.parameters === 'object'
? fn.parameters
: { type: 'object', properties: {} }
parameters: bareOsQvacSanitizeToolParameters(fn.parameters)
})
continue
}
if (typeof o.name === 'string') {
if (typeof o.name === 'string' && o.name) {
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: {} }
parameters: bareOsQvacSanitizeToolParameters(o.parameters)
})
}
}
return out.filter((x) => x && typeof x === 'object' && /** @type {any} */ (x).name)
return out
}
/**
* QVAC completion history is `{ role, content: string }` only.
* OpenAI `content: null` + `tool_calls` fails request validation.
* @param {unknown[]} history
* @returns {{ role: string, content: string }[]}
*/
export function bareOsQvacSanitizeHistory(history) {
if (!Array.isArray(history)) return []
/** @type {{ role: string, content: string }[]} */
const out = []
for (const raw of history) {
if (!raw || typeof raw !== 'object') continue
const m = /** @type {Record<string, unknown>} */ (raw)
const role = typeof m.role === 'string' && m.role ? m.role : 'user'
let content = ''
if (typeof m.content === 'string') content = m.content
else if (Array.isArray(m.content)) {
const parts = []
for (const part of m.content) {
if (typeof part === 'string') parts.push(part)
else if (part && typeof part === 'object') {
const o = /** @type {Record<string, unknown>} */ (part)
if (typeof o.text === 'string') parts.push(o.text)
else if (typeof o.content === 'string') parts.push(o.content)
}
}
content = parts.join('')
} else if (m.content != null && typeof m.content !== 'object') {
content = String(m.content)
}
if (Array.isArray(m.tool_calls) && m.tool_calls.length) {
const chunks = []
for (const tc of m.tool_calls) {
if (!tc || typeof tc !== 'object') continue
const o = /** @type {Record<string, unknown>} */ (tc)
const fn =
o.function && typeof o.function === 'object'
? /** @type {Record<string, unknown>} */ (o.function)
: o
const name = typeof fn.name === 'string' ? fn.name : ''
if (!name) continue
let args = fn.arguments
if (args != null && typeof args !== 'string') {
try {
args = JSON.stringify(args)
} catch {
args = '{}'
}
}
if (typeof args !== 'string' || !args) args = '{}'
let argsObj = args
try {
const parsed = JSON.parse(args)
argsObj = JSON.stringify(
parsed && typeof parsed === 'object' ? parsed : {}
)
} catch {
argsObj = '{}'
}
chunks.push(
'<tool_call>{"name":' +
JSON.stringify(name) +
',"arguments":' +
argsObj +
'}</tool_call>'
)
}
if (chunks.length) {
const serialized = chunks.join('\n')
content = content ? content + '\n' + serialized : serialized
}
}
out.push({ role, content })
}
return out
}
/**
* Match QVAC `detectToolDialectFromName` so we do not override auto-detect
* with the wrong parser (e.g. LFM/DeepSeek).
* @param {string} [modelId]
* @returns {'hermes' | 'qwen35' | 'gemma4' | 'harmony' | 'pythonic' | 'dsml'}
*/
export function bareOsQvacDetectToolDialect(modelId) {
const tag = String(modelId || '').toLowerCase()
if (/qwen3[._-]?[56](?![a-z0-9])/.test(tag)) return 'qwen35'
if (/gemma[-_]?4(?=[^a-z0-9]|$)/.test(tag)) return 'gemma4'
if (/gpt[_-]?oss/.test(tag)) return 'harmony'
if (/deepseek[-_. ]?v(?:4|3[._-]?2)(?![0-9])/.test(tag)) return 'dsml'
if (/lfm[_-]?\d/.test(tag)) return 'pythonic'
return 'hermes'
}
/**
@@ -999,13 +1151,19 @@ export function createBareOsQvacBridge(opts = {}) {
const flatTools = toolsEnabled
? bareOsQvacFlattenTools(o.tools || [])
: []
const history = Array.isArray(o.history) ? o.history : []
const history = bareOsQvacSanitizeHistory(
Array.isArray(o.history) ? o.history : []
)
const toolDialect = bareOsQvacDetectToolDialect(
String(o.modelSrc || loadedModelKey || modelId || '')
)
const run = s.completion({
modelId,
history,
stream: o.stream !== false,
tools: flatTools.length ? flatTools : undefined,
toolDialect,
captureThinking: o.captureThinking !== false
})
activeRequestId = run.requestId || run.id || null