KV cache quantization (--cache-type-k q4_0) and ngram speculative decoding (--spec-type ngram-mod) are high-value llama.cpp features that improve VRAM usage and tokens/sec. Removing them from the shadowing lists allows agents to enable them via llama_extra_args.
161 lines
5.0 KiB
TypeScript
161 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
validateExtraArgs,
|
|
isManagedFlag,
|
|
stripShadowingFlags,
|
|
} from '../inference/llama-args-validator.js';
|
|
import { parseAgentsMd } from '../agents.js';
|
|
|
|
describe('validateExtraArgs', () => {
|
|
describe('deny list — each alias rejected', () => {
|
|
const denied = [
|
|
'-m', '--model',
|
|
'-mu', '--model-url',
|
|
'-dr', '--docker-repo',
|
|
'-hf', '-hfr', '--hf-repo',
|
|
'-hff', '--hf-file',
|
|
'-hfv', '-hfrv', '--hf-repo-v',
|
|
'-hffv', '--hf-file-v',
|
|
'-hft', '--hf-token',
|
|
'-mm', '--mmproj',
|
|
'-mmu', '--mmproj-url',
|
|
'--host', '--port', '--path', '--api-prefix', '--reuse-port',
|
|
'--api-key', '--api-key-file',
|
|
'--ssl-key-file', '--ssl-cert-file',
|
|
'--webui', '--no-webui', '--ui', '--no-ui',
|
|
'--ui-config', '--ui-config-file',
|
|
'--ui-mcp-proxy', '--no-ui-mcp-proxy',
|
|
'--models-dir', '--models-preset', '--models-max',
|
|
'--models-autoload', '--no-models-autoload',
|
|
];
|
|
for (const flag of denied) {
|
|
it(`rejects ${flag}`, () => {
|
|
expect(() => validateExtraArgs([flag])).toThrow(/managed/);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('safe flags accepted', () => {
|
|
const safe = [
|
|
'-c', '--ctx-size', '-ngl', '--gpu-layers',
|
|
'--top-k', '--cache-type-k', '--jinja', '--no-jinja',
|
|
'--spec-draft-n-max', '-fa', '--flash-attn',
|
|
'-t', '--threads', '-np', '--parallel',
|
|
];
|
|
for (const flag of safe) {
|
|
it(`accepts ${flag}`, () => {
|
|
expect(() => validateExtraArgs([flag])).not.toThrow();
|
|
expect(validateExtraArgs([flag])).toEqual([flag]);
|
|
});
|
|
}
|
|
});
|
|
|
|
it('handles --flag=value shape (denies the flag part)', () => {
|
|
expect(() => validateExtraArgs(['--model=evil.gguf'])).toThrow(/managed/);
|
|
});
|
|
|
|
it('handles --flag=value shape (accepts safe flag)', () => {
|
|
expect(validateExtraArgs(['--ctx-size=4096'])).toEqual(['--ctx-size=4096']);
|
|
});
|
|
|
|
it('returns empty array for undefined input', () => {
|
|
expect(validateExtraArgs(undefined)).toEqual([]);
|
|
});
|
|
|
|
it('returns empty array for empty input', () => {
|
|
expect(validateExtraArgs([])).toEqual([]);
|
|
});
|
|
|
|
it('treats negative numbers as values, not flags', () => {
|
|
expect(validateExtraArgs(['--seed', '-1'])).toEqual(['--seed', '-1']);
|
|
});
|
|
});
|
|
|
|
describe('isManagedFlag', () => {
|
|
it('returns true for denied flags', () => {
|
|
expect(isManagedFlag('--model')).toBe(true);
|
|
expect(isManagedFlag('-m')).toBe(true);
|
|
expect(isManagedFlag('--api-key')).toBe(true);
|
|
expect(isManagedFlag('--port')).toBe(true);
|
|
});
|
|
|
|
it('returns false for safe flags', () => {
|
|
expect(isManagedFlag('-c')).toBe(false);
|
|
expect(isManagedFlag('--ctx-size')).toBe(false);
|
|
expect(isManagedFlag('--top-k')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('stripShadowingFlags', () => {
|
|
it('strips auto -c when user supplies -c', () => {
|
|
const result = stripShadowingFlags(['-c', '4096', '--top-k', '40']);
|
|
expect(result).toEqual(['--top-k', '40']);
|
|
});
|
|
|
|
it('retains both when no overlap', () => {
|
|
const result = stripShadowingFlags(['--top-k', '40', '--top-p', '0.95']);
|
|
expect(result).toEqual(['--top-k', '40', '--top-p', '0.95']);
|
|
});
|
|
|
|
it('strips --ctx-size=value form', () => {
|
|
const result = stripShadowingFlags(['--ctx-size=4096']);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('strips boolean --jinja flag (no value consumed)', () => {
|
|
const result = stripShadowingFlags(['--jinja', '--top-k', '40']);
|
|
expect(result).toEqual(['--top-k', '40']);
|
|
});
|
|
|
|
it('respects stripContext=false to keep context flags', () => {
|
|
const result = stripShadowingFlags(['-c', '4096'], { stripContext: false });
|
|
expect(result).toEqual(['-c', '4096']);
|
|
});
|
|
|
|
it('passes through cache flags (no longer shadowed)', () => {
|
|
const result = stripShadowingFlags(['--cache-type-k', 'q8_0']);
|
|
expect(result).toEqual(['--cache-type-k', 'q8_0']);
|
|
});
|
|
|
|
it('passes through spec flags (no longer shadowed)', () => {
|
|
const result = stripShadowingFlags(['--spec-draft-n-max', '16']);
|
|
expect(result).toEqual(['--spec-draft-n-max', '16']);
|
|
});
|
|
});
|
|
|
|
describe('AGENTS.md frontmatter validation', () => {
|
|
it('rejects agent with managed flag in llama_extra_args', () => {
|
|
const md = `## Evil Agent
|
|
---
|
|
llama_extra_args: ["--model", "evil.gguf"]
|
|
---
|
|
You are evil.`;
|
|
const { agents, errors } = parseAgentsMd(md);
|
|
expect(agents).toHaveLength(0);
|
|
expect(errors).toHaveLength(1);
|
|
expect(errors[0]!.reason).toContain('managed');
|
|
});
|
|
|
|
it('accepts agent with safe llama_extra_args', () => {
|
|
const md = `## Good Agent
|
|
---
|
|
llama_extra_args: ["--top-k", "20"]
|
|
---
|
|
You are good.`;
|
|
const { agents, errors } = parseAgentsMd(md);
|
|
expect(errors).toHaveLength(0);
|
|
expect(agents).toHaveLength(1);
|
|
expect(agents[0]!.llama_extra_args).toEqual(['--top-k', '20']);
|
|
});
|
|
|
|
it('agent without llama_extra_args has null field', () => {
|
|
const md = `## Simple Agent
|
|
---
|
|
temperature: 0.5
|
|
---
|
|
You are simple.`;
|
|
const { agents } = parseAgentsMd(md);
|
|
expect(agents[0]!.llama_extra_args).toBeNull();
|
|
});
|
|
});
|