292 lines
12 KiB
JavaScript
292 lines
12 KiB
JavaScript
import { mkdtempSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
process.env.XDG_CONFIG_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-groq-config-'));
|
|
process.env.XDG_DATA_HOME ||= mkdtempSync(path.join(tmpdir(), 'jarvis-groq-data-'));
|
|
import test, { afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { EventEmitter } from 'node:events';
|
|
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
import { createRequire } from 'node:module';
|
|
import { Agent, acquireQvac, releaseQvac, closeQvac, applyAgentInference, qvacStatus } from '../daemon/qvac-master.js';
|
|
import { voiceSettings, SETTINGS_FIELDS } from '../daemon/voice-settings.js';
|
|
import { JarvisDaemon } from '../daemon/index.js';
|
|
import { normalizeSettings, settingVisible, publicSettings } from '../apps/gnome-extension/[email protected]/settings-values.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const groq = require('../vendor/agent-harness/lib/groq.js');
|
|
const webProcess = require('../vendor/agent-harness/lib/web-process.js');
|
|
const engine = Agent.engine;
|
|
|
|
function field(key) {
|
|
return SETTINGS_FIELDS.find((item) => item.key === key);
|
|
}
|
|
|
|
function resetRemote() {
|
|
groq.setHttpsRequest(null);
|
|
engine.configureRemote({ provider: 'local', apiKey: '', model: 'openai/gpt-oss-20b' });
|
|
}
|
|
|
|
afterEach(resetRemote);
|
|
|
|
function mockRequest(handler) {
|
|
return (options, cb) => {
|
|
const req = new EventEmitter();
|
|
req.chunks = [];
|
|
req.write = (chunk) => { req.chunks.push(Buffer.from(chunk)); };
|
|
req.end = () => handler(options, req, cb);
|
|
req.destroy = () => { req.destroyed = true; };
|
|
req.setTimeout = () => {};
|
|
return req;
|
|
};
|
|
}
|
|
|
|
test('Groq settings default to local inference and hide cloud fields', () => {
|
|
const settings = voiceSettings({});
|
|
assert.equal(settings.agentInference, 'local');
|
|
assert.equal(settings.groqApiKey, '');
|
|
assert.equal(settings.groqModel, 'openai/gpt-oss-20b');
|
|
assert.equal(settingVisible(field('groqApiKey'), settings), false);
|
|
assert.equal(settingVisible(field('groqModel'), settings), false);
|
|
assert.equal(settingVisible(field('modelProfile'), settings), true);
|
|
const groqSettings = { ...settings, agentInference: 'groq' };
|
|
assert.equal(settingVisible(field('groqApiKey'), groqSettings), true);
|
|
assert.equal(settingVisible(field('modelProfile'), groqSettings), true);
|
|
assert.equal(settingVisible(field('groqModel'), groqSettings), true);
|
|
const groqModel = field('groqModel');
|
|
assert.equal(groqModel.type, 'choice');
|
|
assert.deepEqual(groqModel.options.map((option) => option.value), [
|
|
'openai/gpt-oss-20b',
|
|
'openai/gpt-oss-120b',
|
|
'llama-3.1-8b-instant',
|
|
'llama-3.3-70b-versatile',
|
|
'qwen/qwen3.6-27b',
|
|
'qwen/qwen3.8-27b',
|
|
'minimaxai/minimax-m2.7',
|
|
'openai/gpt-oss-safeguard-20b',
|
|
]);
|
|
assert.equal(groqModel.options.some((option) => /compound|whisper|orpheus/i.test(option.value)), false);
|
|
assert.equal(publicSettings({ groqApiKey: 'gsk_live_secret' }, SETTINGS_FIELDS).groqApiKey, 'set');
|
|
assert.equal(normalizeSettings({ groqApiKey: ' gsk_abc ' }, SETTINGS_FIELDS).groqApiKey, 'gsk_abc');
|
|
});
|
|
|
|
test('Groq web processing follows a browser crawl and ignores retired search tools', () => {
|
|
assert.equal(webProcess.needsGroqWebProcess([{ role: 'user', content: 'weather' }]), false);
|
|
assert.equal(webProcess.needsGroqWebProcess([
|
|
{ role: 'user', content: 'weather' },
|
|
{ role: 'assistant', content: '', tool_calls: [{ name: 'read_file', arguments: {} }] },
|
|
]), false);
|
|
assert.equal(webProcess.needsGroqWebProcess([
|
|
{ role: 'user', content: 'weather' },
|
|
{ role: 'tool', name: 'browser', content: '{"text":"rain"}', tool_call_id: 'call_1' },
|
|
]), true);
|
|
const rewritten = webProcess.asBrowserCall('web_search', { query: 'weather' });
|
|
assert.equal(rewritten.name, 'browser');
|
|
assert.equal(rewritten.arguments.action, 'navigate');
|
|
assert.equal(rewritten.arguments.url, 'https://duckduckgo.com/?q=weather');
|
|
assert.deepEqual(webProcess.browserToolsOnly([
|
|
{ name: 'browser' },
|
|
{ name: 'read_file' },
|
|
]).map((tool) => tool.name), ['browser']);
|
|
});
|
|
|
|
test('Groq maps harness history and tools to OpenAI chat completions', () => {
|
|
const tools = groq.toOpenAiTools([
|
|
{ name: 'web_search', description: 'Search', parameters: { type: 'object', properties: { query: { type: 'string' } } } },
|
|
]);
|
|
assert.equal(tools[0].type, 'function');
|
|
assert.equal(tools[0].function.name, 'web_search');
|
|
const messages = groq.toOpenAiMessages([
|
|
{ role: 'system', content: 'You are Jarvis.' },
|
|
{ role: 'user', content: 'status', attachments: [{ path: '/tmp/still.jpg' }] },
|
|
{ role: 'assistant', content: '', tool_calls: [{ id: 'call_1', name: 'jarvis_status', arguments: {} }] },
|
|
{ role: 'tool', name: 'jarvis_status', tool_call_id: 'call_1', content: '{"ok":true}' },
|
|
], { vision: false });
|
|
assert.equal(messages[1].content, 'status');
|
|
assert.equal(messages[2].tool_calls[0].function.name, 'jarvis_status');
|
|
assert.equal(messages[3].role, 'tool');
|
|
assert.equal(messages[3].tool_call_id, 'call_1');
|
|
assert.equal(messages[3].name, 'jarvis_status');
|
|
assert.ok(!JSON.stringify(messages).includes('/tmp/still.jpg'));
|
|
});
|
|
|
|
test('Groq vision models attach JPEG stills and text-only models drop them', async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-groq-vision-'));
|
|
const still = path.join(dir, 'still.jpg');
|
|
await writeFile(still, Buffer.from('ffd8ffe000104a464946', 'hex'));
|
|
try {
|
|
const withVision = groq.toOpenAiMessages(
|
|
[{ role: 'user', content: 'What do you see?', attachments: [{ path: still }] }],
|
|
{ vision: true },
|
|
);
|
|
assert.equal(withVision[0].content[0].type, 'text');
|
|
assert.equal(withVision[0].content[1].type, 'image_url');
|
|
assert.match(withVision[0].content[1].image_url.url, /^data:image\/jpeg;base64,/);
|
|
assert.equal(groq.visionModel('qwen/qwen3.6-27b'), true);
|
|
assert.equal(groq.visionModel('openai/gpt-oss-20b'), false);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('Groq requests pin api.groq.com and redact API keys from errors', async () => {
|
|
process.env.GROQ_BASE_URL = 'https://evil.example/openai/v1';
|
|
let hostname = '';
|
|
let auth = '';
|
|
groq.setHttpsRequest(mockRequest((options, req, cb) => {
|
|
hostname = options.hostname;
|
|
auth = options.headers.Authorization;
|
|
const res = new EventEmitter();
|
|
res.statusCode = 401;
|
|
cb(res);
|
|
res.emit('data', Buffer.from(JSON.stringify({ error: { message: 'invalid ' + auth } })));
|
|
res.emit('end');
|
|
}));
|
|
try {
|
|
await assert.rejects(
|
|
groq.complete({
|
|
apiKey: 'gsk_live_secret_value',
|
|
model: 'openai/gpt-oss-20b',
|
|
history: [{ role: 'user', content: 'hi' }],
|
|
}),
|
|
(error) => {
|
|
assert.equal(hostname, 'api.groq.com');
|
|
assert.equal(groq.groqHost(), 'api.groq.com');
|
|
assert.match(error.message, /Groq HTTP 401/);
|
|
assert.doesNotMatch(error.message, /gsk_live_secret_value/);
|
|
assert.doesNotMatch(error.message, /Bearer gsk_live_secret_value/);
|
|
return true;
|
|
},
|
|
);
|
|
} finally {
|
|
delete process.env.GROQ_BASE_URL;
|
|
}
|
|
});
|
|
|
|
test('Groq complete streams content and native tool calls', async () => {
|
|
groq.setHttpsRequest(mockRequest((options, req, cb) => {
|
|
assert.equal(options.hostname, 'api.groq.com');
|
|
assert.equal(options.path, '/openai/v1/chat/completions');
|
|
const body = JSON.parse(Buffer.concat(req.chunks).toString('utf8'));
|
|
assert.equal(body.model, 'openai/gpt-oss-20b');
|
|
assert.equal(body.stream, true);
|
|
assert.equal(body.parallel_tool_calls, false);
|
|
assert.equal(body.reasoning_effort, 'low');
|
|
assert.equal(body.tools[0].function.name, 'web_search');
|
|
const res = new EventEmitter();
|
|
res.statusCode = 200;
|
|
cb(res);
|
|
const frames = [
|
|
'data: {"choices":[{"delta":{"content":"Looking"}}]}\n',
|
|
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_9","function":{"name":"web_search","arguments":"{\\"query\\":\\"news\\"}"}}]}}]}\n',
|
|
'data: [DONE]\n',
|
|
];
|
|
for (const frame of frames) res.emit('data', Buffer.from(frame));
|
|
res.emit('end');
|
|
}));
|
|
const deltas = [];
|
|
const result = await groq.complete({
|
|
apiKey: 'gsk_test',
|
|
model: 'openai/gpt-oss-20b',
|
|
history: [{ role: 'user', content: 'news' }],
|
|
tools: [{ name: 'web_search', description: 'Search', parameters: { type: 'object', properties: { query: { type: 'string' } } } }],
|
|
}, (ev) => deltas.push(ev));
|
|
assert.equal(result.text, 'Looking');
|
|
assert.equal(result.toolCalls[0].name, 'web_search');
|
|
assert.equal(result.toolCalls[0].arguments.query, 'news');
|
|
assert.equal(deltas[0].type, 'contentDelta');
|
|
});
|
|
|
|
test('Groq refuses compound models so tools stay on this computer', async () => {
|
|
await assert.rejects(
|
|
groq.complete({ apiKey: 'gsk_test', model: 'groq/compound', history: [{ role: 'user', content: 'hi' }] }),
|
|
/tools stay on this computer/,
|
|
);
|
|
assert.equal(groq.reasoningEffort('llama-3.3-70b-versatile'), null);
|
|
assert.equal(groq.reasoningEffort('qwen/qwen3.6-27b'), 'none');
|
|
});
|
|
|
|
test('Groq thinks only about a browser result and keeps the local chat model', async () => {
|
|
const original = {
|
|
resources: Agent.engine.resources,
|
|
load: Agent.engine.load,
|
|
close: Agent.engine.close,
|
|
ensureInit: Agent.engine.ensureInit,
|
|
};
|
|
let loads = 0;
|
|
Agent.engine.ensureInit = async () => ({});
|
|
Agent.engine.resources = async () => ({ gpus: [{}] });
|
|
Agent.engine.load = async () => { loads += 1; return { device: 'gpu', modelId: 'local' }; };
|
|
Agent.engine.close = async () => {};
|
|
try {
|
|
engine.configureRemote({ provider: 'groq', apiKey: '', model: 'openai/gpt-oss-20b' });
|
|
await assert.rejects(engine.complete({ history: [{ role: 'user', content: 'hi' }] }), /no model loaded/);
|
|
await assert.rejects(
|
|
engine.complete({ webProcess: true, history: [{ role: 'tool', name: 'browser', content: 'page' }] }),
|
|
/Groq API key/,
|
|
);
|
|
await acquireQvac();
|
|
assert.equal(loads, 1);
|
|
assert.equal(qvacStatus().agentInference, 'groq');
|
|
assert.equal(engine.getLoaded().device, 'groq');
|
|
releaseQvac();
|
|
engine.configureRemote({ provider: 'groq', apiKey: 'gsk_test', model: 'openai/gpt-oss-20b' });
|
|
groq.setHttpsRequest(mockRequest((_options, req, cb) => {
|
|
const body = JSON.parse(Buffer.concat(req.chunks).toString('utf8'));
|
|
assert.equal(body.tools.length, 1);
|
|
assert.equal(body.tools[0].function.name, 'browser');
|
|
assert.match(body.messages.map((m) => m.content).join('\n'), /web-page processing only/);
|
|
const res = new EventEmitter();
|
|
res.statusCode = 200;
|
|
cb(res);
|
|
res.emit('data', Buffer.from('data: {"choices":[{"delta":{"content":"ok"}}]}\ndata: [DONE]\n'));
|
|
res.emit('end');
|
|
}));
|
|
const result = await engine.complete({
|
|
webProcess: true,
|
|
history: [{ role: 'tool', name: 'browser', content: '{"text":"page"}', tool_call_id: 'call_1' }],
|
|
tools: [
|
|
{ name: 'browser', description: 'Browse', parameters: { type: 'object', properties: {} } },
|
|
{ name: 'read_file', description: 'Read', parameters: { type: 'object', properties: {} } },
|
|
],
|
|
});
|
|
assert.equal(result.text, 'ok');
|
|
assert.equal(loads, 1);
|
|
} finally {
|
|
resetRemote();
|
|
await closeQvac();
|
|
Object.assign(Agent.engine, original);
|
|
}
|
|
});
|
|
|
|
test('applyAgentInference honors env keys and runtimeStatus redacts them', async () => {
|
|
const previous = process.env.GROQ_API_KEY;
|
|
process.env.GROQ_API_KEY = 'gsk_env_secret';
|
|
try {
|
|
let unloaded = 0;
|
|
const previousUnload = Agent.engine.unload;
|
|
Agent.engine.unload = async () => { unloaded += 1; };
|
|
const applied = applyAgentInference({ agentInference: 'groq', groqApiKey: 'gsk_file', groqModel: 'llama-3.1-8b-instant' });
|
|
try {
|
|
assert.equal(applied.provider, 'groq');
|
|
assert.equal(applied.model, 'llama-3.1-8b-instant');
|
|
assert.equal(applied.keySet, true);
|
|
await applied.release;
|
|
assert.equal(unloaded, 0);
|
|
} finally { Agent.engine.unload = previousUnload; }
|
|
const daemon = new JarvisDaemon();
|
|
daemon.settings = { ...daemon.settings, agentInference: 'groq', groqApiKey: 'gsk_file_secret' };
|
|
const status = JSON.parse(daemon.runtimeStatus());
|
|
assert.equal(status.agentInference, 'groq');
|
|
assert.equal(status.local, false);
|
|
assert.equal(status.settings.groqApiKey, 'set');
|
|
assert.doesNotMatch(daemon.runtimeStatus(), /gsk_file_secret/);
|
|
assert.doesNotMatch(daemon.runtimeStatus(), /gsk_env_secret/);
|
|
await daemon.close();
|
|
} finally {
|
|
if (previous === undefined) delete process.env.GROQ_API_KEY;
|
|
else process.env.GROQ_API_KEY = previous;
|
|
resetRemote();
|
|
}
|
|
});
|