mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 18:35:04 -10:00
API models: answer room is the connection's own maximum output when the provider lists one, else a generous 32K bounded by the window minus the prompt (was a quarter of the window, at most 8,192, which cut thinking models off with no answer)
This commit is contained in:
1 parent
c99896ae66
commit
df52a28548
3 files changed
+31
-7
No files matched your search
@@ -116,9 +116,18 @@ export const LOCAL_EFFORTS = Object.freeze({ off: null, low: 'low', medium: 'med
|
||||
export const EFFORT_LEVEL = Object.freeze({ off: 'off', low: 'low', medium: 'medium', high: 'high', max: 'max' });
|
||||
export const effortOf = value => EFFORT_LEVEL[String(value ?? '').toLowerCase()] ?? null;
|
||||
|
||||
export function outputBudget(context) {
|
||||
const c = Number.isFinite(context) && context > 0 ? context : 32768;
|
||||
return Math.max(1024, Math.min(8192, Math.floor(c / 4)));
|
||||
/**
|
||||
* Output budget for an API model. The connection's own maximum output is used when it is known (`maxOutput`, read from the provider's model
|
||||
* list). Otherwise a generous 32K, never more than the window minus room for the prompt (a quarter of a small window at most is the prompt's).
|
||||
* It used to be a quarter of the window capped at 8,192, which cut a thinking model off while it was still thinking: `length` and no answer.
|
||||
* The budget is the answer's room: a model that thinks keeps what is left of it for the answer, and the Harness sends the window minus the prompt.
|
||||
*/
|
||||
export const DEFAULT_API_OUTPUT = 32768;
|
||||
export function outputBudget(context, maxOutput = null) {
|
||||
const c = Number.isFinite(context) && context > 0 ? context : null;
|
||||
const window = c === null ? Infinity : Math.max(1024, c - Math.min(4096, Math.floor(c / 4)));
|
||||
if (Number.isFinite(maxOutput) && maxOutput > 0) return Math.max(1024, Math.min(Math.floor(maxOutput), window));
|
||||
return Math.max(1024, Math.min(DEFAULT_API_OUTPUT, window));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,7 +189,7 @@ export function buildProfile({ local = [], connections = [], gate = true, tools
|
||||
...(conn.apiKey || keyless ? { apiKeyEnv: keyEnv } : {}),
|
||||
models: conn.models.map(m => ({
|
||||
id: m.id, name: m.name || m.id,
|
||||
contextWindow: m.contextMax ?? ASSUMED_API_CONTEXT, maxTokens: outputBudget(m.contextMax ?? ASSUMED_API_CONTEXT),
|
||||
contextWindow: m.contextMax ?? ASSUMED_API_CONTEXT, maxTokens: outputBudget(m.contextMax ?? ASSUMED_API_CONTEXT, m.maxOutput),
|
||||
input: ['text'],
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ export function createModelProviders({ directory, changed = () => {}, fetchImpl
|
||||
if (rows.length > 100) fail('Add up to 100 models to one connection.');
|
||||
const models = rows.map(row => {
|
||||
const id = text(row.id, 256); if (!id || /[\r\n]/.test(id)) fail('Every model needs a model ID.');
|
||||
return { id, name: text(row.name) || id, contextMax: numeric(row.contextMax) };
|
||||
return { id, name: text(row.name) || id, contextMax: numeric(row.contextMax), maxOutput: numeric(row.maxOutput) };
|
||||
});
|
||||
if (new Set(models.map(m => m.id)).size !== models.length) fail('Each model ID must be unique within this connection.');
|
||||
return { id: existing?.id ?? randomUUID(), kind, provider: catalog?.id ?? null, name, baseUrl, protocol, apiKey, models };
|
||||
@@ -128,7 +128,7 @@ export function createModelProviders({ directory, changed = () => {}, fetchImpl
|
||||
if (connection.protocol !== 'anthropic' || !data.has_more) break;
|
||||
after = data.last_id; if (!after) break;
|
||||
}
|
||||
const models = [...new Map(rows.filter(m => text(m.id, 256)).map(m => [m.id, { id: text(m.id, 256), name: text(m.display_name ?? m.name) || text(m.id, 256), contextMax: numeric(m.context_length ?? m.max_context_length ?? m.context_window) }])).values()];
|
||||
const models = [...new Map(rows.filter(m => text(m.id, 256)).map(m => [m.id, { id: text(m.id, 256), name: text(m.display_name ?? m.name) || text(m.id, 256), contextMax: numeric(m.context_length ?? m.max_context_length ?? m.context_window), maxOutput: numeric(m.max_output_tokens ?? m.max_completion_tokens ?? m.top_provider?.max_completion_tokens) }])).values()];
|
||||
return { models: models.sort((a, b) => a.name.localeCompare(b.name)) };
|
||||
}
|
||||
async function stream({ selection, message, history = [], systemPrompt = '', signal, onFrame }) {
|
||||
|
||||
@@ -49,7 +49,13 @@ test('routeFor picks the chat\'s own choice', () => {
|
||||
assert.equal(routeFor(p, { connectionId: conn.id, modelId: 'm2' }).modelId, 'm2');
|
||||
assert.equal(routeFor(p, { instanceId: 'inst-1' }).route, 'strata-local');
|
||||
assert.equal(routeFor(p, { connectionId: 'gone', modelId: 'm1' }), null);
|
||||
assert.equal(outputBudget(2048), 1024); assert.equal(outputBudget(1_000_000), 8192);
|
||||
assert.equal(outputBudget(2048), 1536, 'a small window keeps a quarter for the prompt');
|
||||
assert.equal(outputBudget(1_000_000), 32768, 'a big window with no known maximum gets a generous 32K, not 8K');
|
||||
assert.equal(outputBudget(128_000), 32768); assert.equal(outputBudget(null), 32768);
|
||||
assert.equal(outputBudget(16_384), 16_384 - 4096, 'never more than the window minus room for the prompt');
|
||||
assert.equal(outputBudget(200_000, 64_000), 64_000, 'the connection own maximum is used when known');
|
||||
assert.equal(outputBudget(8192, 64_000), 8192 - 2048, 'a known maximum is still bounded by the window');
|
||||
assert.equal(outputBudget(200_000, 4096), 4096, 'a small known maximum is respected');
|
||||
});
|
||||
|
||||
test('the agent environment keeps what a shell needs and drops ambient credentials', () => {
|
||||
@@ -161,3 +167,12 @@ test('the browser and the person\'s own tools have plain sentences, translated i
|
||||
assert.equal(presentTool({ callId: 'c4', name: 'mcp__browser__browser_snapshot', args: {} }).preview, '', 'no empty braces on a card');
|
||||
assert.equal(presentTool({ callId: 'c5', name: 'mcp__browser__browser_evaluate', args: { function: '() => document.title' } }).preview, '', 'a script in the page is not shown as code on the card');
|
||||
});
|
||||
|
||||
test('an API model\'s route gets a generous answer room, or the connection\'s own maximum when the provider listed one', () => {
|
||||
const profile = buildProfile({ local: [], connections: [{ id: 'c1', name: 'C', baseUrl: 'https://x/v1', protocol: 'openai-chat', apiKey: 'k', models: [
|
||||
{ id: 'unknown', contextMax: null }, { id: 'big', contextMax: 200_000, maxOutput: 64_000 }, { id: 'small', contextMax: 16_384 }] }] });
|
||||
const providers = JSON.parse(profile.patch.split('\n').slice(1).join('\n')).find(e => e.id === 'llm-pi-ai').config.providers;
|
||||
const models = Object.values(providers)[0].models;
|
||||
const by = Object.fromEntries(models.map(m => [m.id, m.maxTokens]));
|
||||
assert.equal(by.unknown, 32768); assert.equal(by.big, 64_000); assert.equal(by.small, 16_384 - 4096);
|
||||
});
|
||||
Reference in new issue
Block a user