source dump of claude code
at main 532 lines 18 kB view raw
1import axios from 'axios' 2import { getOauthConfig, OAUTH_BETA_HEADER } from 'src/constants/oauth.js' 3import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' 4import { 5 getIsNonInteractiveSession, 6 getKairosActive, 7 preferThirdPartyAuthentication, 8} from '../bootstrap/state.js' 9import { 10 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 11 logEvent, 12} from '../services/analytics/index.js' 13import { 14 getAnthropicApiKey, 15 getClaudeAIOAuthTokens, 16 handleOAuth401Error, 17 hasProfileScope, 18} from './auth.js' 19import { isInBundledMode } from './bundledMode.js' 20import { getGlobalConfig, saveGlobalConfig } from './config.js' 21import { logForDebugging } from './debug.js' 22import { isEnvTruthy } from './envUtils.js' 23import { 24 getDefaultMainLoopModelSetting, 25 isOpus1mMergeEnabled, 26 type ModelSetting, 27 parseUserSpecifiedModel, 28} from './model/model.js' 29import { getAPIProvider } from './model/providers.js' 30import { isEssentialTrafficOnly } from './privacyLevel.js' 31import { 32 getInitialSettings, 33 getSettingsForSource, 34 updateSettingsForSource, 35} from './settings/settings.js' 36import { createSignal } from './signal.js' 37 38export function isFastModeEnabled(): boolean { 39 return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FAST_MODE) 40} 41 42export function isFastModeAvailable(): boolean { 43 if (!isFastModeEnabled()) { 44 return false 45 } 46 return getFastModeUnavailableReason() === null 47} 48 49type AuthType = 'oauth' | 'api-key' 50 51function getDisabledReasonMessage( 52 disabledReason: FastModeDisabledReason, 53 authType: AuthType, 54): string { 55 switch (disabledReason) { 56 case 'free': 57 return authType === 'oauth' 58 ? 'Fast mode requires a paid subscription' 59 : 'Fast mode unavailable during evaluation. Please purchase credits.' 60 case 'preference': 61 return 'Fast mode has been disabled by your organization' 62 case 'extra_usage_disabled': 63 // Only OAuth users can have extra_usage_disabled; console users don't have this concept 64 return 'Fast mode requires extra usage billing · /extra-usage to enable' 65 case 'network_error': 66 return 'Fast mode unavailable due to network connectivity issues' 67 case 'unknown': 68 return 'Fast mode is currently unavailable' 69 } 70} 71 72export function getFastModeUnavailableReason(): string | null { 73 if (!isFastModeEnabled()) { 74 return 'Fast mode is not available' 75 } 76 77 const statigReason = getFeatureValue_CACHED_MAY_BE_STALE( 78 'tengu_penguins_off', 79 null, 80 ) 81 // Statsig reason has priority over other reasons. 82 if (statigReason !== null) { 83 logForDebugging(`Fast mode unavailable: ${statigReason}`) 84 return statigReason 85 } 86 87 // Previously, fast mode required the native binary (bun build). This is no 88 // longer necessary, but we keep this option behind a flag just in case. 89 if ( 90 !isInBundledMode() && 91 getFeatureValue_CACHED_MAY_BE_STALE('tengu_marble_sandcastle', false) 92 ) { 93 return 'Fast mode requires the native binary · Install from: https://claude.com/product/claude-code' 94 } 95 96 // Not available in the SDK unless explicitly opted in via --settings. 97 // Assistant daemon mode is exempt — it's first-party orchestration, and 98 // kairosActive is set before this check runs (main.tsx:~1626 vs ~3249). 99 if ( 100 getIsNonInteractiveSession() && 101 preferThirdPartyAuthentication() && 102 !getKairosActive() 103 ) { 104 const flagFastMode = getSettingsForSource('flagSettings')?.fastMode 105 if (!flagFastMode) { 106 const reason = 'Fast mode is not available in the Agent SDK' 107 logForDebugging(`Fast mode unavailable: ${reason}`) 108 return reason 109 } 110 } 111 112 // Only available for 1P (not Bedrock/Vertex/Foundry) 113 if (getAPIProvider() !== 'firstParty') { 114 const reason = 'Fast mode is not available on Bedrock, Vertex, or Foundry' 115 logForDebugging(`Fast mode unavailable: ${reason}`) 116 return reason 117 } 118 119 if (orgStatus.status === 'disabled') { 120 if ( 121 orgStatus.reason === 'network_error' || 122 orgStatus.reason === 'unknown' 123 ) { 124 // The org check can fail behind corporate proxies that block the 125 // endpoint. We add CLAUDE_CODE_SKIP_FAST_MODE_NETWORK_ERRORS=1 to 126 // bypass this check in the CC binary. This is OK since we have 127 // another check in the API to error out when disabled by org. 128 if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_FAST_MODE_NETWORK_ERRORS)) { 129 return null 130 } 131 } 132 const authType: AuthType = 133 getClaudeAIOAuthTokens() !== null ? 'oauth' : 'api-key' 134 const reason = getDisabledReasonMessage(orgStatus.reason, authType) 135 logForDebugging(`Fast mode unavailable: ${reason}`) 136 return reason 137 } 138 139 return null 140} 141 142// @[MODEL LAUNCH]: Update supported Fast Mode models. 143export const FAST_MODE_MODEL_DISPLAY = 'Opus 4.6' 144 145export function getFastModeModel(): string { 146 return 'opus' + (isOpus1mMergeEnabled() ? '[1m]' : '') 147} 148 149export function getInitialFastModeSetting(model: ModelSetting): boolean { 150 if (!isFastModeEnabled()) { 151 return false 152 } 153 if (!isFastModeAvailable()) { 154 return false 155 } 156 if (!isFastModeSupportedByModel(model)) { 157 return false 158 } 159 const settings = getInitialSettings() 160 // If per-session opt-in is required, fast mode starts off each session 161 if (settings.fastModePerSessionOptIn) { 162 return false 163 } 164 return settings.fastMode === true 165} 166 167export function isFastModeSupportedByModel( 168 modelSetting: ModelSetting, 169): boolean { 170 if (!isFastModeEnabled()) { 171 return false 172 } 173 const model = modelSetting ?? getDefaultMainLoopModelSetting() 174 const parsedModel = parseUserSpecifiedModel(model) 175 return parsedModel.toLowerCase().includes('opus-4-6') 176} 177 178// --- Fast mode runtime state --- 179// Separate from user preference (settings.fastMode). This tracks the actual 180// operational state: whether we're actively sending fast speed or in cooldown 181// after a rate limit. 182 183export type FastModeRuntimeState = 184 | { status: 'active' } 185 | { status: 'cooldown'; resetAt: number; reason: CooldownReason } 186 187let runtimeState: FastModeRuntimeState = { status: 'active' } 188let hasLoggedCooldownExpiry = false 189 190// --- Cooldown event listeners --- 191export type CooldownReason = 'rate_limit' | 'overloaded' 192 193const cooldownTriggered = 194 createSignal<[resetAt: number, reason: CooldownReason]>() 195const cooldownExpired = createSignal() 196export const onCooldownTriggered = cooldownTriggered.subscribe 197export const onCooldownExpired = cooldownExpired.subscribe 198 199export function getFastModeRuntimeState(): FastModeRuntimeState { 200 if ( 201 runtimeState.status === 'cooldown' && 202 Date.now() >= runtimeState.resetAt 203 ) { 204 if (isFastModeEnabled() && !hasLoggedCooldownExpiry) { 205 logForDebugging('Fast mode cooldown expired, re-enabling fast mode') 206 hasLoggedCooldownExpiry = true 207 cooldownExpired.emit() 208 } 209 runtimeState = { status: 'active' } 210 } 211 return runtimeState 212} 213 214export function triggerFastModeCooldown( 215 resetTimestamp: number, 216 reason: CooldownReason, 217): void { 218 if (!isFastModeEnabled()) { 219 return 220 } 221 runtimeState = { status: 'cooldown', resetAt: resetTimestamp, reason } 222 hasLoggedCooldownExpiry = false 223 const cooldownDurationMs = resetTimestamp - Date.now() 224 logForDebugging( 225 `Fast mode cooldown triggered (${reason}), duration ${Math.round(cooldownDurationMs / 1000)}s`, 226 ) 227 logEvent('tengu_fast_mode_fallback_triggered', { 228 cooldown_duration_ms: cooldownDurationMs, 229 cooldown_reason: 230 reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 231 }) 232 cooldownTriggered.emit(resetTimestamp, reason) 233} 234 235export function clearFastModeCooldown(): void { 236 runtimeState = { status: 'active' } 237} 238 239/** 240 * Called when the API rejects a fast mode request (e.g., 400 "Fast mode is 241 * not enabled for your organization"). Permanently disables fast mode using 242 * the same flow as when the prefetch discovers the org has it disabled. 243 */ 244export function handleFastModeRejectedByAPI(): void { 245 if (orgStatus.status === 'disabled') { 246 return 247 } 248 orgStatus = { status: 'disabled', reason: 'preference' } 249 updateSettingsForSource('userSettings', { fastMode: undefined }) 250 saveGlobalConfig(current => ({ 251 ...current, 252 penguinModeOrgEnabled: false, 253 })) 254 orgFastModeChange.emit(false) 255} 256 257// --- Overage rejection listeners --- 258// Fired when a 429 indicates fast mode was rejected because extra usage 259// (overage billing) is not available. Distinct from org-level disabling. 260const overageRejection = createSignal<[message: string]>() 261export const onFastModeOverageRejection = overageRejection.subscribe 262 263function getOverageDisabledMessage(reason: string | null): string { 264 switch (reason) { 265 case 'out_of_credits': 266 return 'Fast mode disabled · extra usage credits exhausted' 267 case 'org_level_disabled': 268 case 'org_service_level_disabled': 269 return 'Fast mode disabled · extra usage disabled by your organization' 270 case 'org_level_disabled_until': 271 return 'Fast mode disabled · extra usage spending cap reached' 272 case 'member_level_disabled': 273 return 'Fast mode disabled · extra usage disabled for your account' 274 case 'seat_tier_level_disabled': 275 case 'seat_tier_zero_credit_limit': 276 case 'member_zero_credit_limit': 277 return 'Fast mode disabled · extra usage not available for your plan' 278 case 'overage_not_provisioned': 279 case 'no_limits_configured': 280 return 'Fast mode requires extra usage billing · /extra-usage to enable' 281 default: 282 return 'Fast mode disabled · extra usage not available' 283 } 284} 285 286function isOutOfCreditsReason(reason: string | null): boolean { 287 return reason === 'org_level_disabled_until' || reason === 'out_of_credits' 288} 289 290/** 291 * Called when a 429 indicates fast mode was rejected because extra usage 292 * is not available. Permanently disables fast mode (unless the user has 293 * ran out of credits) and notifies with a reason-specific message. 294 */ 295export function handleFastModeOverageRejection(reason: string | null): void { 296 const message = getOverageDisabledMessage(reason) 297 logForDebugging( 298 `Fast mode overage rejection: ${reason ?? 'unknown'}${message}`, 299 ) 300 logEvent('tengu_fast_mode_overage_rejected', { 301 overage_disabled_reason: (reason ?? 302 'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 303 }) 304 // Disable fast mode permanently unless the user has ran out of credits 305 if (!isOutOfCreditsReason(reason)) { 306 updateSettingsForSource('userSettings', { fastMode: undefined }) 307 saveGlobalConfig(current => ({ 308 ...current, 309 penguinModeOrgEnabled: false, 310 })) 311 } 312 overageRejection.emit(message) 313} 314 315export function isFastModeCooldown(): boolean { 316 return getFastModeRuntimeState().status === 'cooldown' 317} 318 319export function getFastModeState( 320 model: ModelSetting, 321 fastModeUserEnabled: boolean | undefined, 322): 'off' | 'cooldown' | 'on' { 323 const enabled = 324 isFastModeEnabled() && 325 isFastModeAvailable() && 326 !!fastModeUserEnabled && 327 isFastModeSupportedByModel(model) 328 if (enabled && isFastModeCooldown()) { 329 return 'cooldown' 330 } 331 if (enabled) { 332 return 'on' 333 } 334 return 'off' 335} 336 337// Disabled reason returned by the API. The API is the canonical source for why 338// fast mode is disabled (free account, admin preference, extra usage not enabled). 339export type FastModeDisabledReason = 340 | 'free' 341 | 'preference' 342 | 'extra_usage_disabled' 343 | 'network_error' 344 | 'unknown' 345 346// In-memory cache of the fast mode status from the API. 347// Distinct from the user's fastMode app state — this represents 348// whether the org *allows* fast mode and why it may be disabled. 349// Modeled as a discriminated union so the invalid state 350// (disabled without a reason) is unrepresentable. 351type FastModeOrgStatus = 352 | { status: 'pending' } 353 | { status: 'enabled' } 354 | { status: 'disabled'; reason: FastModeDisabledReason } 355 356let orgStatus: FastModeOrgStatus = { status: 'pending' } 357 358// Listeners notified when org-level fast mode status changes 359const orgFastModeChange = createSignal<[orgEnabled: boolean]>() 360export const onOrgFastModeChanged = orgFastModeChange.subscribe 361 362type FastModeResponse = { 363 enabled: boolean 364 disabled_reason: FastModeDisabledReason | null 365} 366 367async function fetchFastModeStatus( 368 auth: { accessToken: string } | { apiKey: string }, 369): Promise<FastModeResponse> { 370 const endpoint = `${getOauthConfig().BASE_API_URL}/api/claude_code_penguin_mode` 371 const headers: Record<string, string> = 372 'accessToken' in auth 373 ? { 374 Authorization: `Bearer ${auth.accessToken}`, 375 'anthropic-beta': OAUTH_BETA_HEADER, 376 } 377 : { 'x-api-key': auth.apiKey } 378 379 const response = await axios.get<FastModeResponse>(endpoint, { headers }) 380 return response.data 381} 382 383const PREFETCH_MIN_INTERVAL_MS = 30_000 384let lastPrefetchAt = 0 385let inflightPrefetch: Promise<void> | null = null 386 387/** 388 * Resolve orgStatus from the persisted cache without making any API calls. 389 * Used when startup prefetches are throttled to avoid hitting the network 390 * while still making fast mode availability checks work. 391 */ 392export function resolveFastModeStatusFromCache(): void { 393 if (!isFastModeEnabled()) { 394 return 395 } 396 if (orgStatus.status !== 'pending') { 397 return 398 } 399 const isAnt = process.env.USER_TYPE === 'ant' 400 const cachedEnabled = getGlobalConfig().penguinModeOrgEnabled === true 401 orgStatus = 402 isAnt || cachedEnabled 403 ? { status: 'enabled' } 404 : { status: 'disabled', reason: 'unknown' } 405} 406 407export async function prefetchFastModeStatus(): Promise<void> { 408 // Skip network requests if nonessential traffic is disabled 409 if (isEssentialTrafficOnly()) { 410 return 411 } 412 413 if (!isFastModeEnabled()) { 414 return 415 } 416 417 if (inflightPrefetch) { 418 logForDebugging( 419 'Fast mode prefetch in progress, returning in-flight promise', 420 ) 421 return inflightPrefetch 422 } 423 424 // Service key OAuth sessions lack user:profile scope → endpoint 403s. 425 // Resolve orgStatus from cache and bail before burning the throttle window. 426 // API key auth is unaffected. 427 const apiKey = getAnthropicApiKey() 428 const hasUsableOAuth = 429 getClaudeAIOAuthTokens()?.accessToken && hasProfileScope() 430 if (!hasUsableOAuth && !apiKey) { 431 const isAnt = process.env.USER_TYPE === 'ant' 432 const cachedEnabled = getGlobalConfig().penguinModeOrgEnabled === true 433 orgStatus = 434 isAnt || cachedEnabled 435 ? { status: 'enabled' } 436 : { status: 'disabled', reason: 'preference' } 437 return 438 } 439 440 const now = Date.now() 441 if (now - lastPrefetchAt < PREFETCH_MIN_INTERVAL_MS) { 442 logForDebugging('Skipping fast mode prefetch, fetched recently') 443 return 444 } 445 lastPrefetchAt = now 446 447 const fetchWithCurrentAuth = async (): Promise<FastModeResponse> => { 448 const currentTokens = getClaudeAIOAuthTokens() 449 const auth = 450 currentTokens?.accessToken && hasProfileScope() 451 ? { accessToken: currentTokens.accessToken } 452 : apiKey 453 ? { apiKey } 454 : null 455 if (!auth) { 456 throw new Error('No auth available') 457 } 458 return fetchFastModeStatus(auth) 459 } 460 461 async function doFetch(): Promise<void> { 462 try { 463 let status: FastModeResponse 464 try { 465 status = await fetchWithCurrentAuth() 466 } catch (err) { 467 const isAuthError = 468 axios.isAxiosError(err) && 469 (err.response?.status === 401 || 470 (err.response?.status === 403 && 471 typeof err.response?.data === 'string' && 472 err.response.data.includes('OAuth token has been revoked'))) 473 if (isAuthError) { 474 const failedAccessToken = getClaudeAIOAuthTokens()?.accessToken 475 if (failedAccessToken) { 476 await handleOAuth401Error(failedAccessToken) 477 status = await fetchWithCurrentAuth() 478 } else { 479 throw err 480 } 481 } else { 482 throw err 483 } 484 } 485 486 const previousEnabled = 487 orgStatus.status !== 'pending' 488 ? orgStatus.status === 'enabled' 489 : getGlobalConfig().penguinModeOrgEnabled 490 orgStatus = status.enabled 491 ? { status: 'enabled' } 492 : { 493 status: 'disabled', 494 reason: status.disabled_reason ?? 'preference', 495 } 496 if (previousEnabled !== status.enabled) { 497 // When org disables fast mode, permanently turn off the user's fast mode setting 498 if (!status.enabled) { 499 updateSettingsForSource('userSettings', { fastMode: undefined }) 500 } 501 saveGlobalConfig(current => ({ 502 ...current, 503 penguinModeOrgEnabled: status.enabled, 504 })) 505 orgFastModeChange.emit(status.enabled) 506 } 507 logForDebugging( 508 `Org fast mode: ${status.enabled ? 'enabled' : `disabled (${status.disabled_reason ?? 'preference'})`}`, 509 ) 510 } catch (err) { 511 // On failure: ants default to enabled (don't block internal users). 512 // External users: fall back to the cached penguinModeOrgEnabled value; 513 // if no positive cache, disable with network_error reason. 514 const isAnt = process.env.USER_TYPE === 'ant' 515 const cachedEnabled = getGlobalConfig().penguinModeOrgEnabled === true 516 orgStatus = 517 isAnt || cachedEnabled 518 ? { status: 'enabled' } 519 : { status: 'disabled', reason: 'network_error' } 520 logForDebugging( 521 `Failed to fetch org fast mode status, defaulting to ${orgStatus.status === 'enabled' ? 'enabled (cached)' : 'disabled (network_error)'}: ${err}`, 522 { level: 'error' }, 523 ) 524 logEvent('tengu_org_penguin_mode_fetch_failed', {}) 525 } finally { 526 inflightPrefetch = null 527 } 528 } 529 530 inflightPrefetch = doFetch() 531 return inflightPrefetch 532}