@@ -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
|
||||
|
||||
Reference in New Issue
Block a user