mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
fix(budgeting): add Sonnet 5, Fable 5, and Opus 4.8 to the synthesize context map and brainstorm output caps (#3727)
Wave-assembled from PR #3727 by @p3ob7o. Co-Authored-By: Paolo Belcastro <p3ob7o@users.noreply.github.com>
This commit is contained in:
committed by
Sina Matian
co-authored by
Paolo Belcastro
parent
9b6db85d39
commit
cb07cfda8d
@@ -61,6 +61,10 @@ export const MAX_OUTPUT_TOKENS_CEIL = 32_000;
|
||||
* (with a readable error) instead of the provider's opaque HTTP 400.
|
||||
*/
|
||||
export const ANTHROPIC_OUTPUT_CAPS: Record<string, number> = {
|
||||
'claude-fable-5': 64_000,
|
||||
'claude-opus-5': 32_000,
|
||||
'claude-sonnet-5': 64_000,
|
||||
'claude-opus-4-8': 32_000,
|
||||
'claude-opus-4-7': 32_000,
|
||||
'claude-sonnet-4-6': 64_000,
|
||||
'claude-haiku-4-5': 64_000,
|
||||
|
||||
@@ -31,7 +31,7 @@ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { chat as gatewayChat, validateModelId, type ChatResult } from '../ai/gateway.ts';
|
||||
import { AIConfigError } from '../ai/errors.ts';
|
||||
import { normalizeModelId } from '../model-id.ts';
|
||||
import { normalizeModelId, splitProviderModelId } from '../model-id.ts';
|
||||
import { hasAnthropicKey } from '../ai/anthropic-key.ts';
|
||||
import { basename, join, dirname, isAbsolute, resolve } from 'node:path';
|
||||
import type { BrainEngine } from '../engine.ts';
|
||||
@@ -62,6 +62,10 @@ const SUMMARY_SLUG_RE = new RegExp(`^${PAGE_SLUG_SEG}(\\/${PAGE_SLUG_SEG})*$`, '
|
||||
* resolver returns for known Anthropic aliases.
|
||||
*/
|
||||
const MODEL_CONTEXT_TOKENS: Record<string, number> = {
|
||||
'claude-fable-5': 1_000_000,
|
||||
'claude-opus-5': 1_000_000,
|
||||
'claude-sonnet-5': 1_000_000,
|
||||
'claude-opus-4-8': 1_000_000,
|
||||
'claude-opus-4-7': 1_000_000,
|
||||
'claude-opus-4-6': 1_000_000,
|
||||
'claude-sonnet-4-6': 200_000,
|
||||
@@ -95,14 +99,21 @@ const DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS = 35 * 60 * 1000;
|
||||
* accumulation is out of scope for v0.30.2 (terminal-error classification
|
||||
* catches turn-N blowups; per-turn budget guard is a v0.31+ follow-up).
|
||||
*/
|
||||
function computeChunkCharBudget(
|
||||
export function computeChunkCharBudget(
|
||||
model: string,
|
||||
configMaxPromptTokens: number | null,
|
||||
): number {
|
||||
if (configMaxPromptTokens !== null) {
|
||||
return Math.floor(configMaxPromptTokens * CHARS_PER_TOKEN);
|
||||
}
|
||||
const ctx = MODEL_CONTEXT_TOKENS[model];
|
||||
// Lookup keyed on the bare model name (after prefix strip), mirroring
|
||||
// ANTHROPIC_OUTPUT_CAPS in brainstorm/judges.ts: resolveModel returns
|
||||
// provider-prefixed strings when TIER_DEFAULTS / config values carry a
|
||||
// prefix (the current tier defaults all do), so a raw keyed lookup sent
|
||||
// every tier-resolved brain to the unknown-model fallback — a 5x budget
|
||||
// cut on 1M-context models.
|
||||
const bare = splitProviderModelId(model).model || model;
|
||||
const ctx = MODEL_CONTEXT_TOKENS[bare];
|
||||
if (ctx === undefined) {
|
||||
warnUnknownModelOnce(model);
|
||||
return Math.floor(UNKNOWN_MODEL_BUDGET_TOKENS * CHARS_PER_TOKEN);
|
||||
|
||||
@@ -194,3 +194,30 @@ describe('runJudge wires computeJudgeMaxTokens into chat({maxTokens})', () => {
|
||||
expect(captured[2].maxTokens).toBe(Math.max(LEGACY_MIN_MAX_TOKENS, 50 * 150 + 500)); // 8000
|
||||
});
|
||||
});
|
||||
|
||||
describe('current-generation model caps (Sonnet 5 / Fable 5 / Opus 4.8)', () => {
|
||||
test('500 ideas on claude-sonnet-5: cap binds at its 64K entry, not the 32K unknown-model fallback', () => {
|
||||
// 500 * 150 + 500 = 75_500 → capped. Without a map entry this would
|
||||
// fall to MAX_OUTPUT_TOKENS_CEIL (32_000) and silently halve the
|
||||
// budget on a current-generation model.
|
||||
expect(computeJudgeMaxTokens(500, 'claude-sonnet-5')).toBe(ANTHROPIC_OUTPUT_CAPS['claude-sonnet-5']);
|
||||
expect(ANTHROPIC_OUTPUT_CAPS['claude-sonnet-5']).toBe(64_000);
|
||||
});
|
||||
|
||||
test('provider-prefixed id resolves to the same entry', () => {
|
||||
expect(computeJudgeMaxTokens(500, 'anthropic:claude-sonnet-5')).toBe(64_000);
|
||||
});
|
||||
|
||||
test('claude-fable-5 caps at 64K; claude-opus-4-8 at 32K', () => {
|
||||
expect(computeJudgeMaxTokens(500, 'claude-fable-5')).toBe(64_000);
|
||||
expect(computeJudgeMaxTokens(500, 'claude-opus-4-8')).toBe(32_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('claude-opus-5 cap', () => {
|
||||
test('500 ideas on claude-opus-5: cap binds at its 32K entry (Opus-family convention), prefixed form included', () => {
|
||||
expect(computeJudgeMaxTokens(500, 'claude-opus-5')).toBe(ANTHROPIC_OUTPUT_CAPS['claude-opus-5']);
|
||||
expect(ANTHROPIC_OUTPUT_CAPS['claude-opus-5']).toBe(32_000);
|
||||
expect(computeJudgeMaxTokens(500, 'anthropic:claude-opus-5')).toBe(32_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* computeChunkCharBudget — MODEL_CONTEXT_TOKENS resolution.
|
||||
*
|
||||
* Pins two things:
|
||||
* 1. Current-generation models resolve their real context budget instead
|
||||
* of the unknown-model fallback (the gap this map extension closes).
|
||||
* 2. Provider-prefixed ids resolve to the same entry as bare ids.
|
||||
* resolveModel returns prefixed strings when TIER_DEFAULTS / config
|
||||
* values carry a prefix (the current tier defaults all do); before the
|
||||
* prefix-strip fix, every tier-resolved brain silently fell to the
|
||||
* 180K fallback — a 5x prompt-budget cut on 1M-context models.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { computeChunkCharBudget } from '../src/core/cycle/synthesize.ts';
|
||||
|
||||
// Mirror the module's constants (deliberately duplicated: a silent change to
|
||||
// either should fail this suite, not be absorbed by it).
|
||||
const CHARS_PER_TOKEN = 3.5;
|
||||
const HEADROOM_RATIO = 0.9;
|
||||
const UNKNOWN_BUDGET = Math.floor(180_000 * CHARS_PER_TOKEN);
|
||||
const budgetFor = (contextTokens: number) =>
|
||||
Math.floor(contextTokens * HEADROOM_RATIO * CHARS_PER_TOKEN);
|
||||
|
||||
describe('computeChunkCharBudget — model resolution', () => {
|
||||
test('current-generation 1M-context models get their real budget, not the fallback', () => {
|
||||
for (const model of ['claude-sonnet-5', 'claude-fable-5', 'claude-opus-5', 'claude-opus-4-8']) {
|
||||
expect(computeChunkCharBudget(model, null)).toBe(budgetFor(1_000_000));
|
||||
}
|
||||
});
|
||||
|
||||
test('provider-prefixed ids resolve to the same entry as bare ids', () => {
|
||||
for (const bare of ['claude-sonnet-5', 'claude-opus-5', 'claude-sonnet-4-6']) {
|
||||
expect(computeChunkCharBudget(`anthropic:${bare}`, null)).toBe(
|
||||
computeChunkCharBudget(bare, null),
|
||||
);
|
||||
}
|
||||
// The load-bearing case: a prefixed 1M-context model must NOT fall to
|
||||
// the 180K unknown-model budget.
|
||||
expect(computeChunkCharBudget('anthropic:claude-sonnet-5', null)).toBe(budgetFor(1_000_000));
|
||||
expect(computeChunkCharBudget('anthropic:claude-sonnet-5', null)).not.toBe(UNKNOWN_BUDGET);
|
||||
});
|
||||
|
||||
test('unknown models fall back to the conservative 180K budget', () => {
|
||||
expect(computeChunkCharBudget('openai:gpt-5', null)).toBe(UNKNOWN_BUDGET);
|
||||
expect(computeChunkCharBudget('some-custom-model', null)).toBe(UNKNOWN_BUDGET);
|
||||
});
|
||||
|
||||
test('config override wins over the map', () => {
|
||||
expect(computeChunkCharBudget('claude-sonnet-5', 200_000)).toBe(
|
||||
Math.floor(200_000 * CHARS_PER_TOKEN),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user