blob: 6647293b592f77a8291423653fcbddf33c84efc9 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { PROMPT_COMPOSITION_MAX_TOOLS } from '@maka/core/model-call-attempt';
import {
canonicalizeToolSet,
preparedPromptComposition,
toolSchemaCharsForDiagnostics,
} from '../request-shape.js';
import type { MakaTool } from '../tool-runtime.js';
function tool(name: string): MakaTool {
return { name, description: name, parameters: {}, impl: () => ({}) };
}
const invalid = tool('invalid');
describe('canonicalizeToolSet active allow-list', () => {
test('withholds inactive tools without removing them from the dispatch registry', () => {
const { providerTools, activeTools } = canonicalizeToolSet(
[tool('Read'), tool('Rive'), tool('tool_search')],
invalid,
new Set(['Read', 'tool_search']),
);
assert.deepEqual(activeTools, ['Read', 'tool_search']);
assert.deepEqual(
providerTools.map((candidate) => candidate.name),
['Read', 'Rive', 'tool_search', 'invalid'],
);
});
test('measures only the provider-visible tool schemas', () => {
const tools: MakaTool[] = [
{ ...tool('Read'), parameters: { a: 1 } },
{ ...tool('Rive'), parameters: { big: 'x'.repeat(500) } },
];
assert.ok(
toolSchemaCharsForDiagnostics(tools, ['Read', 'Rive']) >
toolSchemaCharsForDiagnostics(tools, ['Read']) + 400,
);
});
});
describe('prepared prompt composition', () => {
test('folds every semantic part into its bucket and names the tools', () => {
const composition = preparedPromptComposition({
prompt: [
{ role: 'system', content: 'system' },
{ role: 'user', content: 'hello' },
],
tools: [{ name: 'Bash', inputSchema: { type: 'object' } }, { inputSchema: {} }],
providerOptions: { anthropic: { thinking: { type: 'enabled' } } },
});
assert.deepEqual(
composition?.segments.map((segment) => segment.kind),
['system_instructions', 'tool_definitions', 'messages', 'other'],
);
assert.deepEqual(
composition?.tools?.map((tool) => tool.name),
['Bash'],
);
// The unnamed tool's schema is still counted; it just cannot be listed.
assert.ok((composition?.unlabelledToolBytes ?? 0) > 0);
});
test('sizes non-JSON values rather than dropping them', () => {
const composition = preparedPromptComposition({
prompt: [
{
role: 'user',
content: [
{
type: 'file',
data: { type: 'data', data: new Uint8Array([1, 2, 3]) },
mediaType: 'application/octet-stream',
},
],
},
],
});
assert.ok((composition?.segments[0]?.bytes ?? 0) > 0);
});
test('folds a long conversation into one row without losing its bytes', () => {
const prompt = Array.from({ length: 1_000 }, (_, index) => ({
role: 'user',
content: `message-${index}`,
}));
const expectedBytes = prompt.reduce(
(total, message) =>
total + (preparedPromptComposition({ prompt: [message] })?.segments[0]?.bytes ?? 0),
0,
);
const composition = preparedPromptComposition({ prompt });
assert.deepEqual(
composition?.segments.map((segment) => segment.kind),
['messages'],
);
assert.equal(composition?.segments[0]?.bytes, expectedBytes);
});
test('names the largest tools and carries the rest as a counted remainder', () => {
const composition = preparedPromptComposition({
tools: Array.from({ length: PROMPT_COMPOSITION_MAX_TOOLS + 5 }, (_, index) => ({
name: `tool-${String(index).padStart(3, '0')}`,
inputSchema: { type: 'object', padding: 'x'.repeat(index) },
})),
});
assert.equal(composition?.tools?.length, PROMPT_COMPOSITION_MAX_TOOLS);
assert.equal(composition?.remainingTools?.count, 5);
assert.ok((composition?.remainingTools?.bytes ?? 0) > 0);
// Largest first, so what a reader could remove is at the top.
const bytes = composition?.tools?.map((tool) => tool.bytes) ?? [];
assert.deepEqual(
bytes,
[...bytes].sort((left, right) => right - left),
);
// Every tool byte is still accounted for, named or not.
assert.equal(
bytes.reduce((total, size) => total + size, 0) + (composition?.remainingTools?.bytes ?? 0),
composition?.segments.find((segment) => segment.kind === 'tool_definitions')?.bytes,
);
});
test('has nothing to say about an empty request', () => {
assert.equal(preparedPromptComposition({}), undefined);
});
});