135 lines
6.5 KiB
JavaScript
135 lines
6.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createRequire } from 'node:module';
|
|
import { profile } from '../daemon/model-profiles.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const catalog = require('../vendor/agent-harness/lib/catalog.js');
|
|
const toolParse = require('../vendor/agent-harness/lib/tool-parse.js');
|
|
|
|
function qvacTransport(history) {
|
|
return history.map((message) => ({ role: message.role, content: message.content }));
|
|
}
|
|
|
|
test('Qwen tool history replays calls through the QVAC role/content transport', () => {
|
|
const history = [
|
|
{ role: 'assistant', content: '', tool_calls: [{ id: 'c1', name: 'jarvis_status', arguments: { component: 'speaker' } }] },
|
|
{ role: 'tool', tool_call_id: 'c1', content: '{"code":"SPEAKER-7429"}' },
|
|
{ role: 'assistant', content: 'Checking.', tool_calls: [{ type: 'function', function: { name: 'lookup', arguments: '{"count":2,"enabled":false}' } }] },
|
|
];
|
|
const prepared = toolParse.prepareToolHistory(history, 'qwen35');
|
|
const xml = toolParse.qwen35ToolCallXml({ name: 'jarvis_status', arguments: { component: 'speaker' } });
|
|
assert.match(xml, /^<tool_call>\n<function=jarvis_status>\n<parameter=component>\nspeaker\n<\/parameter>\n<\/function>\n<\/tool_call>$/);
|
|
assert.match(prepared[0].content, /<function=jarvis_status>/);
|
|
assert.match(prepared[0].content, /<parameter=component>\nspeaker\n<\/parameter>/);
|
|
assert.match(prepared[2].content, /<parameter=enabled>\nfalse\n<\/parameter>/);
|
|
assert.equal(history[0].content, '');
|
|
assert.equal(prepared[1], history[1]);
|
|
assert.deepEqual(toolParse.prepareToolHistory(prepared, 'qwen35'), prepared);
|
|
assert.equal(toolParse.prepareToolHistory(history, 'hermes'), history);
|
|
|
|
const transported = qvacTransport(prepared);
|
|
assert.deepEqual(Object.keys(transported[0]).sort(), ['content', 'role']);
|
|
const recovered = toolParse.extractCalls(transported[0].content, [{ name: 'jarvis_status' }]);
|
|
assert.equal(recovered.length, 1);
|
|
assert.equal(recovered[0].name, 'jarvis_status');
|
|
assert.equal(recovered[0].arguments.component, 'speaker');
|
|
assert.equal(transported[1].role, 'tool');
|
|
assert.equal(transported[1].content, '{"code":"SPEAKER-7429"}');
|
|
assert.match(transported[2].content, /<tool_call>[\s\S]*<function=lookup>/);
|
|
});
|
|
|
|
test('Qwen3.5 0.8B and 2B use the qwen35 tool dialect and compact-tool reminder', () => {
|
|
assert.equal(profile('laptop-4gb-mm').model, 'qwen3.5-0.8b');
|
|
assert.equal(profile('laptop-8gb-mm').model, 'qwen3.5-2b');
|
|
for (const id of ['qwen3.5-0.8b', 'qwen3.5-2b']) {
|
|
assert.equal(catalog.toolDialectFor(id), 'qwen35');
|
|
assert.equal(catalog.findCatalogEntry(id).tools, true);
|
|
assert.equal(catalog.isCompactToolModel(id), true);
|
|
assert.equal(catalog.findCatalogEntry(id).ctxSize, 16384);
|
|
}
|
|
assert.match(toolParse.FORMAT_REMINDER, /<function=TOOL_NAME>/);
|
|
assert.deepEqual(catalog.compactGenerationParams('qwen3.5-0.8b'), { temp: 0.55 });
|
|
assert.deepEqual(catalog.compactGenerationParams('qwen3.5-2b'), { temp: 0.55 });
|
|
assert.deepEqual(catalog.compactGenerationParams('qwen3.5-4b'), {});
|
|
assert.equal(catalog.reasoningBudgetForModel('qwen3.5-0.8b'), 128);
|
|
assert.equal(catalog.reasoningBudgetForModel('qwen3.5-2b'), 192);
|
|
assert.equal(catalog.reasoningBudgetForModel('qwen3.5-4b'), 320);
|
|
assert.equal(catalog.reasoningBudgetForModel('qwen3.5-9b'), 448);
|
|
assert.equal(catalog.reasoningBudgetForModel('gemma4-4b'), null);
|
|
const compactGen = catalog.generationParamsForModel('qwen3.5-0.8b', { predict: 512, seed: 7 });
|
|
assert.equal(compactGen.temp, 0.55);
|
|
assert.equal(compactGen.reasoning_budget, 128);
|
|
assert.equal(compactGen.repeat_penalty, 1.15);
|
|
assert.equal(compactGen.predict, 512);
|
|
assert.equal(compactGen.seed, 7);
|
|
const largeGen = catalog.generationParamsForModel('qwen3.5-4b', { predict: 512 });
|
|
assert.equal(largeGen.reasoning_budget, 320);
|
|
assert.equal(largeGen.predict, 512);
|
|
assert.equal(largeGen.repeat_penalty, 1.15);
|
|
const tiny = catalog.filterToolsForModel(
|
|
[{ name: 'web_search' }, { name: 'todo_write' }, { name: 'browser' }, { name: 'webcam' }, { name: 'qvac_capability' }, { name: 'cu_drag' }],
|
|
'qwen3.5-0.8b',
|
|
);
|
|
assert.deepEqual(tiny.map((t) => t.name), ['todo_write', 'browser', 'webcam']);
|
|
});
|
|
|
|
test('Qwen3.5 compact models recover tool calls nested in think tags', () => {
|
|
const tools = [{ name: 'web_search' }, { name: 'web_fetch' }];
|
|
const thinking = `<think>
|
|
Need a current answer.
|
|
<tool_call>
|
|
<function=web_search>
|
|
<parameter=query>Ubuntu 26.04 release</parameter>
|
|
</function>
|
|
</tool_call>
|
|
</think>`;
|
|
const recovered = toolParse.recover({ thinking, text: '', tools });
|
|
assert.equal(recovered.calls.length, 1);
|
|
assert.equal(recovered.calls[0].name, 'web_search');
|
|
assert.equal(recovered.calls[0].arguments.query, 'Ubuntu 26.04 release');
|
|
});
|
|
|
|
test('SDK thinking events use text and recover think tags from content', () => {
|
|
const events = require('../vendor/agent-harness/lib/events.js');
|
|
const sdkThink = events.normalizeCompletionEvent({ type: 'thinkingDelta', seq: 1, text: 'Considering the lookup.' });
|
|
assert.equal(sdkThink.type, 'thinkingDelta');
|
|
assert.equal(sdkThink.delta, 'Considering the lookup.');
|
|
const first = events.expandThinkEvents({ type: 'contentDelta', delta: 'Hello <think>reason' }, { inThink: false, carry: '' });
|
|
assert.deepEqual(first.events, [
|
|
{ type: 'contentDelta', delta: 'Hello ' },
|
|
{ type: 'thinkingDelta', delta: 'reason' },
|
|
]);
|
|
assert.equal(first.state.inThink, true);
|
|
const next = events.expandThinkEvents({ type: 'contentDelta', delta: 'ing</think>Answer' }, first.state);
|
|
assert.deepEqual(next.events, [
|
|
{ type: 'thinkingDelta', delta: 'ing' },
|
|
{ type: 'contentDelta', delta: 'Answer' },
|
|
]);
|
|
assert.equal(next.state.inThink, false);
|
|
});
|
|
|
|
test('click and type aliases recover as computer-use tools', () => {
|
|
const tools = [{ name: 'cu_click' }, { name: 'cu_type' }, { name: 'cu_observe' }];
|
|
const calls = toolParse.extractCalls(
|
|
'<tool_call><function=click><parameter=ref>\nr4\n</parameter></function></tool_call>',
|
|
tools,
|
|
);
|
|
assert.equal(calls[0].name, 'cu_click');
|
|
assert.equal(calls[0].arguments.ref, 'r4');
|
|
const typed = toolParse.extractCalls(
|
|
'<tool_call><function=type><parameter=text>\nhi\n</parameter></function></tool_call>',
|
|
tools,
|
|
);
|
|
assert.equal(typed[0].name, 'cu_type');
|
|
});
|
|
|
|
test('camera alias recovers as webcam', () => {
|
|
const tools = [{ name: 'webcam' }, { name: 'cu_observe' }];
|
|
const calls = toolParse.extractCalls(
|
|
'<tool_call><function=camera></function></tool_call>',
|
|
tools,
|
|
);
|
|
assert.equal(calls[0].name, 'webcam');
|
|
});
|