93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
/**
|
|
* Read tool output limits. Two caps apply to text reads:
|
|
*
|
|
* | limit | default | checks | cost | on overflow |
|
|
* |---------------|---------|---------------------------|---------------|-----------------|
|
|
* | maxSizeBytes | 256 KB | TOTAL FILE SIZE (not out) | 1 stat | throws pre-read |
|
|
* | maxTokens | 25000 | actual output tokens | API roundtrip | throws post-read|
|
|
*
|
|
* Known mismatch: maxSizeBytes gates on total file size, not the slice.
|
|
* Tested truncating instead of throwing for explicit-limit reads that
|
|
* exceed the byte cap (#21841, Mar 2026). Reverted: tool error rate
|
|
* dropped but mean tokens rose — the throw path yields a ~100-byte error
|
|
* tool-result while truncation yields ~25K tokens of content at the cap.
|
|
*/
|
|
import memoize from 'lodash-es/memoize.js'
|
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
|
import { MAX_OUTPUT_SIZE } from 'src/utils/file.js'
|
|
export const DEFAULT_MAX_OUTPUT_TOKENS = 25000
|
|
|
|
/**
|
|
* Env var override for max output tokens. Returns undefined when unset/invalid
|
|
* so the caller can fall through to the next precedence tier.
|
|
*/
|
|
function getEnvMaxTokens(): number | undefined {
|
|
const override = process.env.CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS
|
|
if (override) {
|
|
const parsed = parseInt(override, 10)
|
|
if (!isNaN(parsed) && parsed > 0) {
|
|
return parsed
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export type FileReadingLimits = {
|
|
maxTokens: number
|
|
maxSizeBytes: number
|
|
includeMaxSizeInPrompt?: boolean
|
|
targetedRangeNudge?: boolean
|
|
}
|
|
|
|
/**
|
|
* Default limits for Read tool when the ToolUseContext doesn't supply an
|
|
* override. Memoized so the GrowthBook value is fixed at first call — avoids
|
|
* the cap changing mid-session as the flag refreshes in the background.
|
|
*
|
|
* Precedence for maxTokens: env var > GrowthBook > DEFAULT_MAX_OUTPUT_TOKENS.
|
|
* (Env var is a user-set override, should beat experiment infrastructure.)
|
|
*
|
|
* Defensive: each field is individually validated; invalid values fall
|
|
* through to the hardcoded defaults (no route to cap=0).
|
|
*/
|
|
export const getDefaultFileReadingLimits = memoize((): FileReadingLimits => {
|
|
const override =
|
|
getFeatureValue_CACHED_MAY_BE_STALE<Partial<FileReadingLimits> | null>(
|
|
'tengu_amber_wren',
|
|
{},
|
|
)
|
|
|
|
const maxSizeBytes =
|
|
typeof override?.maxSizeBytes === 'number' &&
|
|
Number.isFinite(override.maxSizeBytes) &&
|
|
override.maxSizeBytes > 0
|
|
? override.maxSizeBytes
|
|
: MAX_OUTPUT_SIZE
|
|
|
|
const envMaxTokens = getEnvMaxTokens()
|
|
const maxTokens =
|
|
envMaxTokens ??
|
|
(typeof override?.maxTokens === 'number' &&
|
|
Number.isFinite(override.maxTokens) &&
|
|
override.maxTokens > 0
|
|
? override.maxTokens
|
|
: DEFAULT_MAX_OUTPUT_TOKENS)
|
|
|
|
const includeMaxSizeInPrompt =
|
|
typeof override?.includeMaxSizeInPrompt === 'boolean'
|
|
? override.includeMaxSizeInPrompt
|
|
: undefined
|
|
|
|
const targetedRangeNudge =
|
|
typeof override?.targetedRangeNudge === 'boolean'
|
|
? override.targetedRangeNudge
|
|
: undefined
|
|
|
|
return {
|
|
maxSizeBytes,
|
|
maxTokens,
|
|
includeMaxSizeInPrompt,
|
|
targetedRangeNudge,
|
|
}
|
|
})
|