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:
Niko1221 committed 2026-10-16 14:09:37 +02:00
1 parent c99896ae66
commit df52a28548
3 files changed
+31 -7

No files matched your search

+13 -4
View File
@@ -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'],
})),
};
+2 -2
View File
@@ -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 }) {
+16 -1
View File
@@ -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);
});