π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes filterAllowedSdkBetas, modelSupportsISP, modelSupportsContextManagement, modelSupportsStructuredOutputs, and modelSupportsAutoMode (and more) β mainly functions, hooks, or classes. Dependencies touch bun:bundle, lodash-es, and src. It composes internal code from bootstrap, constants, auth, context, and envUtils (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { feature } from 'bun:bundle' import memoize from 'lodash-es/memoize.js' import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE, getFeatureValue_CACHED_MAY_BE_STALE,
π€ Exports (heuristic)
filterAllowedSdkBetasmodelSupportsISPmodelSupportsContextManagementmodelSupportsStructuredOutputsmodelSupportsAutoModegetToolSearchBetaHeadershouldIncludeFirstPartyOnlyBetasshouldUseGlobalCacheScopegetAllModelBetasgetModelBetasgetBedrockExtraBodyParamsBetasgetMergedBetasclearBetasCaches
π External import roots
Package roots from from "β¦" (relative paths omitted).
bun:bundlelodash-essrc
π₯οΈ Source preview
import { feature } from 'bun:bundle'
import memoize from 'lodash-es/memoize.js'
import {
checkStatsigFeatureGate_CACHED_MAY_BE_STALE,
getFeatureValue_CACHED_MAY_BE_STALE,
} from 'src/services/analytics/growthbook.js'
import { getIsNonInteractiveSession, getSdkBetas } from '../bootstrap/state.js'
import {
BEDROCK_EXTRA_PARAMS_HEADERS,
CLAUDE_CODE_20250219_BETA_HEADER,
CLI_INTERNAL_BETA_HEADER,
CONTEXT_1M_BETA_HEADER,
CONTEXT_MANAGEMENT_BETA_HEADER,
INTERLEAVED_THINKING_BETA_HEADER,
PROMPT_CACHING_SCOPE_BETA_HEADER,
REDACT_THINKING_BETA_HEADER,
STRUCTURED_OUTPUTS_BETA_HEADER,
SUMMARIZE_CONNECTOR_TEXT_BETA_HEADER,
TOKEN_EFFICIENT_TOOLS_BETA_HEADER,
TOOL_SEARCH_BETA_HEADER_1P,
TOOL_SEARCH_BETA_HEADER_3P,
WEB_SEARCH_BETA_HEADER,
} from '../constants/betas.js'
import { OAUTH_BETA_HEADER } from '../constants/oauth.js'
import { isClaudeAISubscriber } from './auth.js'
import { has1mContext } from './context.js'
import { isEnvDefinedFalsy, isEnvTruthy } from './envUtils.js'
import { getCanonicalName } from './model/model.js'
import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
import { getAPIProvider } from './model/providers.js'
import { getInitialSettings } from './settings/settings.js'
/**
* SDK-provided betas that are allowed for API key users.
* Only betas in this list can be passed via SDK options.
*/
const ALLOWED_SDK_BETAS = [CONTEXT_1M_BETA_HEADER]
/**
* Filter betas to only include those in the allowlist.
* Returns allowed and disallowed betas separately.
*/
function partitionBetasByAllowlist(betas: string[]): {
allowed: string[]
disallowed: string[]
} {
const allowed: string[] = []
const disallowed: string[] = []
for (const beta of betas) {
if (ALLOWED_SDK_BETAS.includes(beta)) {
allowed.push(beta)
} else {
disallowed.push(beta)
}
}
return { allowed, disallowed }
}
/**
* Filter SDK betas to only include allowed ones.
* Warns about disallowed betas and subscriber restrictions.
* Returns undefined if no valid betas remain or if user is a subscriber.
*/
export function filterAllowedSdkBetas(
sdkBetas: string[] | undefined,
): string[] | undefined {
if (!sdkBetas || sdkBetas.length === 0) {
return undefined
}
if (isClaudeAISubscriber()) {
// biome-ignore lint/suspicious/noConsole: intentional warning
console.warn(
'Warning: Custom betas are only available for API key users. Ignoring provided betas.',
)
return undefined
}
const { allowed, disallowed } = partitionBetasByAllowlist(sdkBetas)
for (const beta of disallowed) {
// biome-ignore lint/suspicious/noConsole: intentional warning
console.warn(
`Warning: Beta header '${beta}' is not allowed. Only the following betas are supported: ${ALLOWED_SDK_BETAS.join(', ')}`,
)
}
return allowed.length > 0 ? allowed : undefined
}
// Generally, foundry supports all 1P features;
// however out of an abundance of caution, we do not enable any which are behind an experiment
export function modelSupportsISP(model: string): boolean {
const supported3P = get3PModelCapabilityOverride(
model,
'interleaved_thinking',
)
if (supported3P !== undefined) {
return supported3P
}
const canonical = getCanonicalName(model)
const provider = getAPIProvider()
// Foundry supports interleaved thinking for all models
if (provider === 'foundry') {
return true
}
if (provider === 'firstParty') {
return !canonical.includes('claude-3-')
}
return (
canonical.includes('claude-opus-4') || canonical.includes('claude-sonnet-4')
)
}
function vertexModelSupportsWebSearch(model: string): boolean {
const canonical = getCanonicalName(model)
// Web search only supported on Claude 4.0+ models on Vertex
return (
canonical.includes('claude-opus-4') ||
canonical.includes('claude-sonnet-4') ||
canonical.includes('claude-haiku-4')
)
}
// Context management is supported on Claude 4+ models
export function modelSupportsContextManagement(model: string): boolean {
const canonical = getCanonicalName(model)
const provider = getAPIProvider()
if (provider === 'foundry') {
return true
}
if (provider === 'firstParty') {
return !canonical.includes('claude-3-')
}
return (
canonical.includes('claude-opus-4') ||
canonical.includes('claude-sonnet-4') ||
canonical.includes('claude-haiku-4')
)
}
// @[MODEL LAUNCH]: Add the new model ID to this list if it supports structured outputs.
export function modelSupportsStructuredOutputs(model: string): boolean {
const canonical = getCanonicalName(model)
const provider = getAPIProvider()
// Structured outputs only supported on firstParty and Foundry (not Bedrock/Vertex yet)
if (provider !== 'firstParty' && provider !== 'foundry') {
return false
}
return (
canonical.includes('claude-sonnet-4-6') ||
canonical.includes('claude-sonnet-4-5') ||
canonical.includes('claude-opus-4-1') ||
canonical.includes('claude-opus-4-5') ||
canonical.includes('claude-opus-4-6') ||
canonical.includes('claude-haiku-4-5')
)
}
// @[MODEL LAUNCH]: Add the new model if it supports auto mode (specifically PI probes) β ask in #proj-claude-code-safety-research.
export function modelSupportsAutoMode(model: string): boolean {
if (feature('TRANSCRIPT_CLASSIFIER')) {
const m = getCanonicalName(model)
// External: firstParty-only at launch (PI probes not wired for
// Bedrock/Vertex/Foundry yet). Checked before allowModels so the GB
// override can't enable auto mode on unsupported providers.
if (process.env.USER_TYPE !== 'ant' && getAPIProvider() !== 'firstParty') {
return false
}
// GrowthBook override: tengu_auto_mode_config.allowModels force-enables
// auto mode for listed models, bypassing the denylist/allowlist below.
// Exact model IDs (e.g. "claude-strudel-v6-p") match only that model;
// canonical names (e.g. "claude-strudel") match the whole family.
const config = getFeatureValue_CACHED_MAY_BE_STALE<{
allowModels?: string[]
}>('tengu_auto_mode_config', {})
const rawLower = model.toLowerCase()
if (
config?.allowModels?.some(
am => am.toLowerCase() === rawLower || am.toLowerCase() === m,
)
) {
return true
}
if (process.env.USER_TYPE === 'ant') {
// Denylist: block known-unsupported claude models, allow everything else (ant-internal models etc.)
if (m.includes('claude-3-')) return false
// claude-*-4 not followed by -[6-9]: blocks bare -4, -4-YYYYMMDD, -4@, -4-0 thru -4-5
if (/claude-(opus|sonnet|haiku)-4(?!-[6-9])/.test(m)) return false
return true
}
// External allowlist (firstParty already checked above).
return /^claude-(opus|sonnet)-4-6/.test(m)
}
return false
}
/**
* Get the correct tool search beta header for the current API provider.
* - Claude API / Foundry: advanced-tool-use-2025-11-20
* - Vertex AI / Bedrock: tool-search-tool-2025-10-19
*/
export function getToolSearchBetaHeader(): string {
const provider = getAPIProvider()
if (provider === 'vertex' || provider === 'bedrock') {
return TOOL_SEARCH_BETA_HEADER_3P
}
return TOOL_SEARCH_BETA_HEADER_1P
}
/**
* Check if experimental betas should be included.
* These are betas that are only available on firstParty provider
* and may not be supported by proxies or other providers.
*/
export function shouldIncludeFirstPartyOnlyBetas(): boolean {
return (
(getAPIProvider() === 'firstParty' || getAPIProvider() === 'foundry') &&
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS)
)
}
/**
* Global-scope prompt caching is firstParty only. Foundry is excluded because
* GrowthBook never bucketed Foundry users into the rollout experiment β the
* treatment data is firstParty-only.
*/
export function shouldUseGlobalCacheScope(): boolean {
return (
getAPIProvider() === 'firstParty' &&
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS)
)
}
export const getAllModelBetas = memoize((model: string): string[] => {
const betaHeaders = []
const isHaiku = getCanonicalName(model).includes('haiku')
const provider = getAPIProvider()
const includeFirstPartyOnlyBetas = shouldIncludeFirstPartyOnlyBetas()
if (!isHaiku) {
betaHeaders.push(CLAUDE_CODE_20250219_BETA_HEADER)
if (
process.env.USER_TYPE === 'ant' &&
process.env.CLAUDE_CODE_ENTRYPOINT === 'cli'
) {
if (CLI_INTERNAL_BETA_HEADER) {
betaHeaders.push(CLI_INTERNAL_BETA_HEADER)
}
}
}
if (isClaudeAISubscriber()) {
betaHeaders.push(OAUTH_BETA_HEADER)
}
if (has1mContext(model)) {
betaHeaders.push(CONTEXT_1M_BETA_HEADER)
}
if (
!isEnvTruthy(process.env.DISABLE_INTERLEAVED_THINKING) &&
modelSupportsISP(model)
) {
betaHeaders.push(INTERLEAVED_THINKING_BETA_HEADER)
}
// Skip the API-side Haiku thinking summarizer β the summary is only used
// for ctrl+o display, which interactive users rarely open. The API returns
// redacted_thinking blocks instead; AssistantRedactedThinkingMessage already
// renders those as a stub. SDK / print-mode keep summaries because callers
// may iterate over thinking content. Users can opt back in via settings.json
// showThinkingSummaries.
if (
includeFirstPartyOnlyBetas &&
modelSupportsISP(model) &&
!getIsNonInteractiveSession() &&
getInitialSettings().showThinkingSummaries !== true
) {
betaHeaders.push(REDACT_THINKING_BETA_HEADER)
}
// POC: server-side connector-text summarization (anti-distillation). The
// API buffers assistant text between tool calls, summarizes it, and returns
// the summary with a signature so the original can be restored on subsequent
// turns β same mechanism as thinking blocks. Ant-only while we measure
// TTFT/TTLT/capacity; betas already flow to tengu_api_success for splitting.
// Backend independently requires Capability.ANTHROPIC_INTERNAL_RESEARCH.
//
// USE_CONNECTOR_TEXT_SUMMARIZATION is tri-state: =1 forces on (opt-in even
// if GB is off), =0 forces off (opt-out of a GB rollout you were bucketed
// into), unset defers to GB.
if (
SUMMARIZE_CONNECTOR_TEXT_BETA_HEADER &&
process.env.USER_TYPE === 'ant' &&
includeFirstPartyOnlyBetas &&
!isEnvDefinedFalsy(process.env.USE_CONNECTOR_TEXT_SUMMARIZATION) &&
(isEnvTruthy(process.env.USE_CONNECTOR_TEXT_SUMMARIZATION) ||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_prism', false))
) {
betaHeaders.push(SUMMARIZE_CONNECTOR_TEXT_BETA_HEADER)
}
// Add context management beta for tool clearing (ant opt-in) or thinking preservation
const antOptedIntoToolClearing =
isEnvTruthy(process.env.USE_API_CONTEXT_MANAGEMENT) &&
process.env.USER_TYPE === 'ant'
const thinkingPreservationEnabled = modelSupportsContextManagement(model)
if (
shouldIncludeFirstPartyOnlyBetas() &&
(antOptedIntoToolClearing || thinkingPreservationEnabled)
) {
betaHeaders.push(CONTEXT_MANAGEMENT_BETA_HEADER)
}
// Add strict tool use beta if experiment is enabled.
// Gate on includeFirstPartyOnlyBetas: CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS
// already strips schema.strict from tool bodies at api.ts's choke point, but
// this header was escaping that kill switch. Proxy gateways that look like
// firstParty but forward to Vertex reject this header with 400.
// github.com/deshaw/anthropic-issues/issues/5
const strictToolsEnabled =
checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_tool_pear')
// 3P default: false. API rejects strict + token-efficient-tools together
// (tool_use.py:139), so these are mutually exclusive β strict wins.
const tokenEfficientToolsEnabled =
!strictToolsEnabled &&
getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_json_tools', false)
if (
includeFirstPartyOnlyBetas &&
modelSupportsStructuredOutputs(model) &&
strictToolsEnabled
) {
betaHeaders.push(STRUCTURED_OUTPUTS_BETA_HEADER)
}
// JSON tool_use format (FC v3) β ~4.5% output token reduction vs ANTML.
// Sends the v2 header (2026-03-28) added in anthropics/anthropic#337072 to
// isolate the CC A/B cohort from ~9.2M/week existing v1 senders. Ant-only
// while the restored JsonToolUseOutputParser soaks.
if (
process.env.USER_TYPE === 'ant' &&
includeFirstPartyOnlyBetas &&
tokenEfficientToolsEnabled
) {
betaHeaders.push(TOKEN_EFFICIENT_TOOLS_BETA_HEADER)
}
// Add web search beta for Vertex Claude 4.0+ models only
if (provider === 'vertex' && vertexModelSupportsWebSearch(model)) {
betaHeaders.push(WEB_SEARCH_BETA_HEADER)
}
// Foundry only ships models that already support Web Search
if (provider === 'foundry') {
betaHeaders.push(WEB_SEARCH_BETA_HEADER)
}
// Always send the beta header for 1P. The header is a no-op without a scope field.
if (includeFirstPartyOnlyBetas) {
betaHeaders.push(PROMPT_CACHING_SCOPE_BETA_HEADER)
}
// If ANTHROPIC_BETAS is set, split it by commas and add to betaHeaders.
// This is an explicit user opt-in, so honor it regardless of model.
if (process.env.ANTHROPIC_BETAS) {
betaHeaders.push(
...process.env.ANTHROPIC_BETAS.split(',')
.map(_ => _.trim())
.filter(Boolean),
)
}
return betaHeaders
})
export const getModelBetas = memoize((model: string): string[] => {
const modelBetas = getAllModelBetas(model)
if (getAPIProvider() === 'bedrock') {
return modelBetas.filter(b => !BEDROCK_EXTRA_PARAMS_HEADERS.has(b))
}
return modelBetas
})
export const getBedrockExtraBodyParamsBetas = memoize(
(model: string): string[] => {
const modelBetas = getAllModelBetas(model)
return modelBetas.filter(b => BEDROCK_EXTRA_PARAMS_HEADERS.has(b))
},
)
/**
* Merge SDK-provided betas with auto-detected model betas.
* SDK betas are read from global state (set via setSdkBetas in main.tsx).
* The betas are pre-filtered by filterAllowedSdkBetas which handles
* subscriber checks and allowlist validation with warnings.
*
* @param options.isAgenticQuery - When true, ensures the beta headers needed
* for agentic queries are present. For non-Haiku models these are already
* included by getAllModelBetas(); for Haiku they're excluded since
* non-agentic calls (compaction, classifiers, token estimation) don't need them.
*/
export function getMergedBetas(
model: string,
options?: { isAgenticQuery?: boolean },
): string[] {
const baseBetas = [...getModelBetas(model)]
// Agentic queries always need claude-code and cli-internal beta headers.
// For non-Haiku models these are already in baseBetas; for Haiku they're
// excluded by getAllModelBetas() since non-agentic Haiku calls don't need them.
if (options?.isAgenticQuery) {
if (!baseBetas.includes(CLAUDE_CODE_20250219_BETA_HEADER)) {
baseBetas.push(CLAUDE_CODE_20250219_BETA_HEADER)
}
if (
process.env.USER_TYPE === 'ant' &&
process.env.CLAUDE_CODE_ENTRYPOINT === 'cli' &&
CLI_INTERNAL_BETA_HEADER &&
!baseBetas.includes(CLI_INTERNAL_BETA_HEADER)
) {
baseBetas.push(CLI_INTERNAL_BETA_HEADER)
}
}
const sdkBetas = getSdkBetas()
if (!sdkBetas || sdkBetas.length === 0) {
return baseBetas
}
// Merge SDK betas without duplicates (already filtered by filterAllowedSdkBetas)
return [...baseBetas, ...sdkBetas.filter(b => !baseBetas.includes(b))]
}
export function clearBetasCaches(): void {
getAllModelBetas.cache?.clear?.()
getModelBetas.cache?.clear?.()
getBedrockExtraBodyParamsBetas.cache?.clear?.()
}