mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
fix(think): extend 16K output headroom to OpenAI reasoning models (#3916)
maxOutputTokensFor() gave the 16000-token output budget only to thinking-by-default Anthropic Claude 5 models; every other model kept the 4000 default. OpenAI reasoning models (the gpt-5 family and the numbered o-series) bill internal reasoning tokens as output tokens counting against max_tokens, so the 4000 cap starved think's actual answer the same way. Add OPENAI_REASONING_MODEL_RE alongside the existing Claude-5 regex, deliberately scoped to the gpt-5 family + numbered o-series with the non-reasoning ChatGPT -chat snapshots (gpt-5-chat-latest, gpt-5.2-chat-latest) excluded. Defaults (4000/16000) are unchanged and no config knob is added; gpt-4o, other providers, and provider-prefixed spellings outside openai:/openai/ keep 4000. Tests pin the new 16000 cases (colon + slash forms), the chat-snapshot and codex-mini-latest exclusions, and name/version boundaries (gpt-50, o3foo, bare model names, openrouter-nested ids). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+12
-1
@@ -171,8 +171,19 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 4000;
|
||||
// keeps 4000.
|
||||
const THINKING_DEFAULT_MAX_OUTPUT_TOKENS = 16000;
|
||||
const THINKING_BY_DEFAULT_MODEL_RE = /^anthropic[:/]claude-[a-z0-9]+-5(?:[.-]|$)/i;
|
||||
// OpenAI reasoning models spend output budget on internal reasoning tokens
|
||||
// the same way — reasoning tokens are billed as output and count against
|
||||
// `max_tokens` — so they get the same headroom. Deliberately scoped to the
|
||||
// gpt-5 family and the numbered o-series only; anything else (gpt-4o, the
|
||||
// non-reasoning `*-chat` snapshots like gpt-5-chat-latest, other providers'
|
||||
// reasoning models routed through their own recipes) keeps the conservative
|
||||
// 4000 default.
|
||||
const OPENAI_REASONING_MODEL_RE = /^openai[:/](?:gpt-5|o[0-9]+)(?:[.-]|$)/i;
|
||||
const OPENAI_CHAT_SNAPSHOT_RE = /-chat(?:-|$)/i; // gpt-5-chat-latest, gpt-5.2-chat-latest
|
||||
export function maxOutputTokensFor(modelStr: string): number {
|
||||
return THINKING_BY_DEFAULT_MODEL_RE.test(modelStr)
|
||||
const openaiReasoning =
|
||||
OPENAI_REASONING_MODEL_RE.test(modelStr) && !OPENAI_CHAT_SNAPSHOT_RE.test(modelStr);
|
||||
return THINKING_BY_DEFAULT_MODEL_RE.test(modelStr) || openaiReasoning
|
||||
? THINKING_DEFAULT_MAX_OUTPUT_TOKENS
|
||||
: DEFAULT_MAX_OUTPUT_TOKENS;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Pins `maxOutputTokensFor` — the per-model output-token budget `runThink`
|
||||
* passes to `client.create`. Thinking-by-default Claude 5 models
|
||||
* (`anthropic:claude-*-5`) spend a large share of the budget on internal
|
||||
* reasoning before emitting an answer, so the 4000 default left `think` with
|
||||
* empty/truncated text. They now get 16000; everything else stays 4000.
|
||||
* (`anthropic:claude-*-5`) and OpenAI reasoning models (gpt-5 family,
|
||||
* o-series) spend a large share of the budget on internal reasoning before
|
||||
* emitting an answer, so the 4000 default left `think` with empty/truncated
|
||||
* text. They get 16000; everything else stays 4000.
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { maxOutputTokensFor } from '../src/core/think/index.ts';
|
||||
@@ -17,12 +18,42 @@ describe('maxOutputTokensFor — thinking-default headroom', () => {
|
||||
expect(maxOutputTokensFor('anthropic/claude-sonnet-5')).toBe(16000); // slash form
|
||||
});
|
||||
|
||||
test('non-Claude-5 and non-Anthropic keep 4000', () => {
|
||||
test('OpenAI reasoning models (gpt-5 family, o-series) get 16000', () => {
|
||||
expect(maxOutputTokensFor('openai:gpt-5')).toBe(16000);
|
||||
expect(maxOutputTokensFor('openai:gpt-5.2')).toBe(16000);
|
||||
expect(maxOutputTokensFor('openai:gpt-5.5')).toBe(16000);
|
||||
expect(maxOutputTokensFor('openai:gpt-5-mini')).toBe(16000);
|
||||
expect(maxOutputTokensFor('openai:o1')).toBe(16000);
|
||||
expect(maxOutputTokensFor('openai:o3')).toBe(16000);
|
||||
expect(maxOutputTokensFor('openai:o4-mini')).toBe(16000);
|
||||
expect(maxOutputTokensFor('openai/gpt-5.2')).toBe(16000); // slash form
|
||||
});
|
||||
|
||||
test('non-Claude-5 and non-reasoning models keep 4000', () => {
|
||||
expect(maxOutputTokensFor('anthropic:claude-opus-4-8')).toBe(4000);
|
||||
expect(maxOutputTokensFor('anthropic:claude-haiku-4-5')).toBe(4000);
|
||||
expect(maxOutputTokensFor('anthropic:claude-sonnet-4-6')).toBe(4000);
|
||||
expect(maxOutputTokensFor('anthropic:claude-3-haiku')).toBe(4000);
|
||||
expect(maxOutputTokensFor('openai:gpt-4o')).toBe(4000);
|
||||
expect(maxOutputTokensFor('openai:gpt-4o-mini')).toBe(4000);
|
||||
expect(maxOutputTokensFor('openai:gpt-4.1')).toBe(4000);
|
||||
// Non-reasoning ChatGPT snapshots of the gpt-5 family stay at 4000.
|
||||
expect(maxOutputTokensFor('openai:gpt-5-chat-latest')).toBe(4000);
|
||||
expect(maxOutputTokensFor('openai:gpt-5.2-chat-latest')).toBe(4000);
|
||||
// Scope is the gpt-5 family + numbered o-series only — other OpenAI
|
||||
// reasoning-capable ids (e.g. codex-mini-latest) keep the conservative
|
||||
// default until deliberately added.
|
||||
expect(maxOutputTokensFor('openai:codex-mini-latest')).toBe(4000);
|
||||
// Version/name boundaries: `gpt-50` and `o3foo` are not gpt-5 / o3.
|
||||
expect(maxOutputTokensFor('openai:gpt-50')).toBe(4000);
|
||||
expect(maxOutputTokensFor('openai:o3foo')).toBe(4000);
|
||||
// Other providers' reasoning models are out of scope here — the routed
|
||||
// provider recipe, not this budget, is what changes for them.
|
||||
expect(maxOutputTokensFor('deepseek:deepseek-reasoner')).toBe(4000);
|
||||
// Prefix must be the openai provider — a bare model name or another
|
||||
// provider's gpt-5 spelling doesn't match.
|
||||
expect(maxOutputTokensFor('o3')).toBe(4000);
|
||||
expect(maxOutputTokensFor('gpt-5.2')).toBe(4000);
|
||||
expect(maxOutputTokensFor('openrouter:openai/gpt-5.2')).toBe(4000);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user