mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 16:55:51 +00:00
* fix: 终端内容溢出 viewport 时的重影 bug 主屏幕模式下 frame 持续溢出 viewport 时,cursor-restore LF 把内容滚入 scrollback 导致相对光标追踪漂移,可见区 diff 落到错误行产生重影(重复 banner / 错位)。 扩展 log-update overflow 分支为无条件 fullReset(含 \x1b[3J 清 scrollback), 并将主屏 self-healing 清屏从 ERASE_SCREEN (CSI 2 J) 换成 ERASE_DOWN (CSI J), 避免 xterm.js / VSCode 集成终端的 scrollback 边界副作用。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 删除 3 个孤立诊断脚本 - scripts/verify-autofix-pr.ts: 一次性 autofix-pr 验证脚本,全仓零引用 - scripts/smoke-test-commands.ts: 开发期冒烟测试脚本,无任何 import - scripts/probe-subscription-endpoints.ts: 手动 endpoint 探针,无引用 均不在 package.json scripts、build.ts、vite.config.ts、CI workflows 中。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 self-hosted-runner stub 及其 cli.tsx fast-path - 删除 src/self-hosted-runner/main.ts(自动生成的 Promise.resolve() stub) - 同步移除 src/entrypoints/cli.tsx 中 feature('SELF_HOSTED_RUNNER') 守卫的 fast-path 分支 - 该 flag 不在 build.ts DEFAULT_BUILD_FEATURES 也不在 dev 默认列表,所有默认配置下整段为构建期死代码 删除 stub 单独会留下未解析的动态 import,必须协同拆除。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 删除 agentSdkTypes 中三个 not-implemented stub 移除 watchScheduledTasks、buildMissedTaskNotification、connectRemoteControl 三个 stub 函数(函数体仅 throw new Error('not implemented')),以及仅被这些 stub 引用的孤儿类型(ScheduledTasksHandle、ConnectRemoteControlOptions、RemoteControlHandle、InboundPrompt 等)。 全仓零外部引用。buildMissedTaskNotification 在 src/utils/cronScheduler.ts 有真实可用实现,未受影响。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 Cursor.ts 中未引用的 kill ring 访问器 - 删除 getKillRingItem、getKillRingSize、clearKillRing、canYankPop(全仓零引用的独立 export) - 移除 VIM_WORD_CHAR_REGEX 的 export 关键字(仍由 isVimWordChar 内部使用,保留常量本体) kill ring 特性本身仍活跃(getLastKill/pushToKillRing/yankPop 在 useSearchInput/useTextInput 使用),仅这几个孤儿 helper 未接入。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 insights.ts 中未引用的导出 - 删除 deduplicateSessionBranches(全仓零调用,含 JSDoc) - 删除 buildExportData(全仓零调用,原 S3 上传路径实际用 HTML 而非 JSON) - InsightsExport 仅移除 export 关键字(保留类型本体,仍作为内部返回类型) Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 autonomyCommandSpec.ts 中未引用的导出 - 删除 AUTONOMY_CLI(CLI 子命令描述对象,零引用;handler 仅用 AUTONOMY_USAGE) - 删除 AUTONOMY_COMMAND_DESCRIPTION(值已在 main.tsx:5181 内联) - ParsedAutonomyCommand 仅移除 export 关键字(保留类型作为 parseAutonomyArgs 返回类型) Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 binaryCheck/claudeAiLimits/codeIndexing 中未引用的导出 - binaryCheck.ts: 删除 clearBinaryCache(零调用,binaryCache 仍由 isBinaryInstalled 使用) - claudeAiLimits.ts: 删除 RATE_LIMIT_DISPLAY_NAMES 常量 + getRateLimitDisplayName(互为唯一消费者) - codeIndexing.ts: 删除 detectCodeIndexingFromMcpTool(同胞 detectCodeIndexingFromCommand/McpServerName 仍活跃) Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除多处仅内部使用的 export 关键字 下列符号均仅在本文件内被引用,export 关键字冗余;保留符号本体不动: - internalLogging.ts: getContainerId(line 88 内部调用) - api/errors.ts: isMediaSizeError(line 151 内部调用) - api/withRetry.ts: parseMaxTokensContextOverflowError(line 389/724 内部调用) - statsCache.ts: STATS_CACHE_VERSION(7 处内部使用) - startupProfiler.ts: logStartupPerf(line 128 内部调用) - bashCommandHelpers.ts: CommandIdentityCheckers(3 处内部参数类型) Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 清理注释代码块与 legacy shim 注释代码(已死的、引用不存在符号的注释块): - Onboarding.tsx: 注释化的 preflight if-block(引用不存在的 preflightStep) - ultraplan.tsx: 两处引用不存在符号的注释(ULTRAPLAN_INSTRUCTIONS、getUltraplanModel) - types/hooks.ts: 禁用的 type-fest IsEqual 类型断言块 - types/global.d.ts: 已被真实模块取代的 Ultraplan ambient declares - types/textInputTypes.ts: 注释化的 onMessage interface 成员 legacy shim: - cli/bg.ts: 删除 handleBgFlag 别名 export(同胞 handleBgStart 已被所有调用点使用) Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 ccshareResume stub 及 main.tsx 的 ccshare fast-path - 删除 src/utils/ccshareResume.ts(parseCcshareId 恒返回 null、loadCcshare 恒抛错的 stub) - 同步移除 src/main.tsx 中 USER_TYPE === 'ant' 守卫下的 if (ccshareId) {...} else {...} 双分支 - 提升 else 块(文件路径 resume 处理)为直接进入 if (options.resume) 块内 ccshare 是 Anthropic 内部特性(go/ccshare URL),stub 未实现导致 ccshareId 恒为 null,整个 ccshare 分支永不进入;保留的文件路径 resume 路径不变。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 environment-runner stub 及其 cli.tsx fast-path 与 self-hosted-runner 相同模式的 sibling(工作流 1 verifier 建议同步处理): - 删除 src/environment-runner/main.ts(自动生成的 Promise.resolve() stub) - 同步移除 src/entrypoints/cli.tsx 中 feature('BYOC_ENVIRONMENT_RUNNER') 守卫的 fast-path 分支 - 清理两个空目录(src/self-hosted-runner/、src/environment-runner/) BYOC_ENVIRONMENT_RUNNER flag 不在 build.ts DEFAULT_BUILD_FEATURES 也不在 dev 默认列表,所有默认配置下整段为构建期死代码。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 删除孤立诊断脚本 probe-local-wiring.ts #!/usr/bin/env bun shebang 的手动诊断脚本,全仓零引用,不在 package.json/build.ts/vite.config.ts/CI workflows 中。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 ultrareview preflight stub 及其测试 - 删除 src/services/api/ultrareviewPreflight.ts(自动生成的 stub) - 删除 src/commands/review/UltrareviewPreflightDialog.tsx(依赖前者的 UI stub) - 删除 src/services/api/__tests__/ultrareviewPreflight.test.ts(测试已删代码) - 同步移除 ultrareviewCommand.test.tsx 中对 UltrareviewPreflightDialog 的 mock Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 cachedMCConfig stub 及 prompts.ts 的 CACHED_MICROCOMPACT 死代码 - 删除 src/services/compact/cachedMCConfig.ts(自动生成的 stub) - 同步移除 src/constants/prompts.ts 中依赖该 stub 的代码: - getCachedMCConfigForFRC 变量(feature('CACHED_MICROCOMPACT') 守卫的 require) - getFunctionResultClearingSection 函数(约 18 行) - systemPrompt 数组中的 frc section 调用与注册 CACHED_MICROCOMPACT 不在 build.ts DEFAULT_BUILD_FEATURES 也不在 dev 默认列表,所有默认配置下整段为构建期死代码。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 goalAudit stub 及其测试引用 - 删除 src/services/goal/goalAudit.ts(导出 COMPLETION_AUDIT_RULES/BLOCKED_AUDIT_RULES/isGoalTerminal 等未引用的 stub) - 同步移除 tests/integration/goal-lifecycle.test.ts 中对 goalAudit 的 import 和一个测试用例(budget_limited is terminal) Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 删除 agentSdkTypes 第二批 not-implemented stub 移除运行时函数体仅为 throw new Error 或 placeholder 的 stub: - createSdkMcpToolDefinition、createSdkMcpServer - query 函数重载与实现 - unstable_v2_* 系列函数 - session 操作 stub(getSessionMessages/listSessions/getSessionInfo/renameSession/tagSession/forkSession) - AbortError 类 保留所有 export type 重导出和类型别名(仍是公共类型面)。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 Tool.ts 中 backwards-compat 重导出 shim 删除 "// Re-export progress types for backwards compatibility" 注释块及其重导出语句。所有消费方已直接从 src/types/tools.js 导入,无需重导出转发。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 bootstrap/state.ts 中 4 个未引用的 export - clearRegisteredHooks(STATE.registeredHooks 仍由其他函数管理) - getInvokedSkills(getInvokedSkillsForAgent 是活跃入口) - getSessionSource(setSessionSource 仍活跃,sessionSource state 字段保留) - markScrollActivity(scrollDraining/getIsScrollDraining/waitForScrollDrain 仍活跃) 仅删除孤儿访问器,不动模块级 state 副作用。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 src/ 下多处未引用的导出 涉及 18 个文件,每处均为独立的 unreferenced export 删除或 export 关键字冗余移除: - bridge/bridgeStatusUtil.ts、components/TrustDialog/utils.ts、context/stats.tsx - keybindings/loadUserBindings.ts、memdir/paths.ts、remote/sdkMessageAdapter.ts - services/acp/utils.ts(删除 nodeToWebReadable,全仓零引用) - services/api/metricsOptOut.ts、services/lsp/LSPDiagnosticRegistry.ts、services/lsp/manager.ts - services/mcp/utils.ts、services/skillLearning/projectContext.ts - services/teamMemorySync/secretScanner.ts、services/teamMemorySync/watcher.ts - skills/loadSkillsDir.ts、utils/attachments.ts、utils/filePersistence/filePersistence.ts - utils/messageQueueManager.ts Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * chore: 移除 packages/ 下多处未引用的导出 涉及 11 个 workspace 包文件,每处均为独立的 unreferenced export 删除或 export 关键字冗余移除: - @ant/ink/core/termio/csi.ts(eraseLine) - acp-link/manager/types.ts、acp-link/ws-message.ts - builtin-tools/AgentTool/agentMemory.ts、BashTool/bashSecurity.ts、BashTool/sedEditParser.ts - builtin-tools/ConfigTool/supportedSettings.ts、FileEditTool/utils.ts - remote-control-server/store.ts、transport/event-bus.ts、types/messages.ts Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * Revert "fix: 终端内容溢出 viewport 时的重影 bug" This reverts commit3d18e1da58. * revert: 移除主屏幕周期性 self-healing 重绘 回退f69c7051中引入的 ink.tsx self-healing 机制(lastMainScreenHealTime 字段 + 每 5 秒触发全量重绘 + needsEraseBeforePaint 主屏幕分支)。该机制在 workflow 面板持续刷新场景下表现为可见的"重复刷新",且修复效果不稳定。 alt-screen 的 needsEraseBeforePaint 路径和 prevFrameContaminated 字段保留, 它们仍服务于 handleResize / layout shift / selection 高亮。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * fix: /workflows 面板默认只显示运行中 run,根治 tab 行乱码 之前几次渲染层修复都失败,因为没动 tab 列表的数据源:打开 /workflows 会 自动 hydrate 最多 20 个历史 done/killed run,全部塞进一行 TabsBar,超出 终端宽度后 Ink 把字符画到屏外造成重影乱码。 - selectors.ts 加 filterActiveRuns(只留 status === 'running')和 capTabsForDisplay(超额 fold 成 +N)两个 pure function - WorkflowsPanel 接线 activeRuns:focus clamp、focused、nextTab/prevTab、 TabsBar 全部基于过滤后的 activeRuns - TabsBar 复用 truncateLabel 限制每个 tab 名 18 字符 + 最多 6 个 tab, 多余显示 +N,从结构上钉死单行总宽度 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * fix: /workflows 面板 phase 状态在脚本省略 phase() 时显示错乱 ultracode canonical pipeline 脚本常在 agent() 直接传 opts.phase 而不调 phase() hook,导致 phase_started 从未发出;同时 phase_done 只在下次 phase() 触发,上一 个 phase 在 run.phases 里一直停在 running。mergePhases 之前把 actual 当权威, 于是出现 "Map 8/8 全 done 还显示 running、Find 1/4 running 反而显示 pending"。 改为派生层修复:mergePhases 新增 derivePhaseStatus——actual.status==='done' 权威;否则有 agents 就按 agents 状态推(全 done→done,否则 running);否则看 actual 是否 running。再补一层遍历,让只在 agents 上出现的 phase 也进 sidebar。 不改 store 状态语义,已有 state.json 无需迁移。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> * docs: 更新 readme * fix: ACP 模式未读取 settings.local.json entry.ts 在 ACP 握手期调用的 applySafeConfigEnvironmentVariables 触发了 loadSettingsFromDisk,此时 getOriginalCwd() 还是进程启动 cwd(非项目目录), 导致 localSettings/projectSettings 按错误路径解析为空并被 session cache 锁住, 后续 createSession 里 setOriginalCwd 也无法纠正。在 setOriginalCwd 与 chdir 之后清缓存并重新应用,让 settings.local.json 和项目级 env 对 readSettingsPermissionMode 及下游可见。 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win> --------- Co-authored-by: glm-5.2 <zai-org@claude-code-best.win>
514 lines
16 KiB
TypeScript
514 lines
16 KiB
TypeScript
import { APIError } from '@anthropic-ai/sdk'
|
|
import type { MessageParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
|
import isEqual from 'lodash-es/isEqual.js'
|
|
import { getIsNonInteractiveSession } from '../bootstrap/state.js'
|
|
import { isClaudeAISubscriber } from '../utils/auth.js'
|
|
import { getModelBetas } from '../utils/betas.js'
|
|
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
|
import { logError } from '../utils/log.js'
|
|
import { getSmallFastModel } from '../utils/model/model.js'
|
|
import { isEssentialTrafficOnly } from '../utils/privacyLevel.js'
|
|
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from './analytics/index.js'
|
|
import { logEvent } from './analytics/index.js'
|
|
import { getAPIMetadata } from './api/claude.js'
|
|
import { getAnthropicClient } from './api/client.js'
|
|
import { anthropicAdapter } from './providerUsage/adapters/anthropic.js'
|
|
import { updateProviderBuckets } from './providerUsage/store.js'
|
|
import {
|
|
processRateLimitHeaders,
|
|
shouldProcessRateLimits,
|
|
} from './rateLimitMocking.js'
|
|
|
|
// Re-export message functions from centralized location
|
|
export {
|
|
getRateLimitErrorMessage,
|
|
getRateLimitWarning,
|
|
getUsingOverageText,
|
|
} from './rateLimitMessages.js'
|
|
|
|
type QuotaStatus = 'allowed' | 'allowed_warning' | 'rejected'
|
|
|
|
type RateLimitType =
|
|
| 'five_hour'
|
|
| 'seven_day'
|
|
| 'seven_day_opus'
|
|
| 'seven_day_sonnet'
|
|
| 'overage'
|
|
|
|
export type { RateLimitType }
|
|
|
|
type EarlyWarningThreshold = {
|
|
utilization: number // 0-1 scale: trigger warning when usage >= this
|
|
timePct: number // 0-1 scale: trigger warning when time elapsed <= this
|
|
}
|
|
|
|
type EarlyWarningConfig = {
|
|
rateLimitType: RateLimitType
|
|
claimAbbrev: '5h' | '7d'
|
|
windowSeconds: number
|
|
thresholds: EarlyWarningThreshold[]
|
|
}
|
|
|
|
// Early warning configurations in priority order (checked first to last)
|
|
// Used as fallback when server doesn't send surpassed-threshold header
|
|
// Warns users when they're consuming quota faster than the time window allows
|
|
const EARLY_WARNING_CONFIGS: EarlyWarningConfig[] = [
|
|
{
|
|
rateLimitType: 'five_hour',
|
|
claimAbbrev: '5h',
|
|
windowSeconds: 5 * 60 * 60,
|
|
thresholds: [{ utilization: 0.9, timePct: 0.72 }],
|
|
},
|
|
{
|
|
rateLimitType: 'seven_day',
|
|
claimAbbrev: '7d',
|
|
windowSeconds: 7 * 24 * 60 * 60,
|
|
thresholds: [
|
|
{ utilization: 0.75, timePct: 0.6 },
|
|
{ utilization: 0.5, timePct: 0.35 },
|
|
{ utilization: 0.25, timePct: 0.15 },
|
|
],
|
|
},
|
|
]
|
|
|
|
// Maps claim abbreviations to rate limit types for header-based detection
|
|
const EARLY_WARNING_CLAIM_MAP: Record<string, RateLimitType> = {
|
|
'5h': 'five_hour',
|
|
'7d': 'seven_day',
|
|
overage: 'overage',
|
|
}
|
|
|
|
/**
|
|
* Calculate what fraction of a time window has elapsed.
|
|
* Used for time-relative early warning fallback.
|
|
* @param resetsAt - Unix epoch timestamp in seconds when the limit resets
|
|
* @param windowSeconds - Duration of the window in seconds
|
|
* @returns fraction (0-1) of the window that has elapsed
|
|
*/
|
|
function computeTimeProgress(resetsAt: number, windowSeconds: number): number {
|
|
const nowSeconds = Date.now() / 1000
|
|
const windowStart = resetsAt - windowSeconds
|
|
const elapsed = nowSeconds - windowStart
|
|
return Math.max(0, Math.min(1, elapsed / windowSeconds))
|
|
}
|
|
|
|
// Reason why overage is disabled/rejected
|
|
// These values come from the API's unified limiter
|
|
export type OverageDisabledReason =
|
|
| 'overage_not_provisioned' // Overage is not provisioned for this org or seat tier
|
|
| 'org_level_disabled' // Organization doesn't have overage enabled
|
|
| 'org_level_disabled_until' // Organization overage temporarily disabled
|
|
| 'out_of_credits' // Organization has insufficient credits
|
|
| 'seat_tier_level_disabled' // Seat tier doesn't have overage enabled
|
|
| 'member_level_disabled' // Account specifically has overage disabled
|
|
| 'seat_tier_zero_credit_limit' // Seat tier has a zero credit limit
|
|
| 'group_zero_credit_limit' // Resolved group limit has a zero credit limit
|
|
| 'member_zero_credit_limit' // Account has a zero credit limit
|
|
| 'org_service_level_disabled' // Org service specifically has overage disabled
|
|
| 'org_service_zero_credit_limit' // Org service has a zero credit limit
|
|
| 'no_limits_configured' // No overage limits configured for account
|
|
| 'unknown' // Unknown reason, should not happen
|
|
|
|
export type ClaudeAILimits = {
|
|
status: QuotaStatus
|
|
// unifiedRateLimitFallbackAvailable is currently used to warn users that set
|
|
// their model to Opus whenever they are about to run out of quota. It does
|
|
// not change the actual model that is used.
|
|
unifiedRateLimitFallbackAvailable: boolean
|
|
resetsAt?: number
|
|
rateLimitType?: RateLimitType
|
|
utilization?: number
|
|
overageStatus?: QuotaStatus
|
|
overageResetsAt?: number
|
|
overageDisabledReason?: OverageDisabledReason
|
|
isUsingOverage?: boolean
|
|
surpassedThreshold?: number
|
|
}
|
|
|
|
// Exported for testing only
|
|
export let currentLimits: ClaudeAILimits = {
|
|
status: 'allowed',
|
|
unifiedRateLimitFallbackAvailable: false,
|
|
isUsingOverage: false,
|
|
}
|
|
|
|
/**
|
|
* Raw per-window utilization from response headers, tracked on every API
|
|
* response (unlike currentLimits.utilization which is only set when a warning
|
|
* threshold fires). Exposed to statusline scripts via getRawUtilization().
|
|
*/
|
|
type RawWindowUtilization = {
|
|
utilization: number // 0-1 fraction
|
|
resets_at: number // unix epoch seconds
|
|
}
|
|
type RawUtilization = {
|
|
five_hour?: RawWindowUtilization
|
|
seven_day?: RawWindowUtilization
|
|
}
|
|
let rawUtilization: RawUtilization = {}
|
|
|
|
export function getRawUtilization(): RawUtilization {
|
|
return rawUtilization
|
|
}
|
|
|
|
function extractRawUtilization(headers: globalThis.Headers): RawUtilization {
|
|
const result: RawUtilization = {}
|
|
for (const [key, abbrev] of [
|
|
['five_hour', '5h'],
|
|
['seven_day', '7d'],
|
|
] as const) {
|
|
const util = headers.get(
|
|
`anthropic-ratelimit-unified-${abbrev}-utilization`,
|
|
)
|
|
const reset = headers.get(`anthropic-ratelimit-unified-${abbrev}-reset`)
|
|
if (util !== null && reset !== null) {
|
|
result[key] = { utilization: Number(util), resets_at: Number(reset) }
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
type StatusChangeListener = (limits: ClaudeAILimits) => void
|
|
export const statusListeners: Set<StatusChangeListener> = new Set()
|
|
|
|
export function emitStatusChange(limits: ClaudeAILimits) {
|
|
currentLimits = limits
|
|
statusListeners.forEach(listener => listener(limits))
|
|
const hoursTillReset = Math.round(
|
|
(limits.resetsAt ? limits.resetsAt - Date.now() / 1000 : 0) / (60 * 60),
|
|
)
|
|
|
|
logEvent('tengu_claudeai_limits_status_changed', {
|
|
status:
|
|
limits.status as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
unifiedRateLimitFallbackAvailable: limits.unifiedRateLimitFallbackAvailable,
|
|
hoursTillReset,
|
|
})
|
|
}
|
|
|
|
async function makeTestQuery() {
|
|
const model = getSmallFastModel()
|
|
const anthropic = await getAnthropicClient({
|
|
maxRetries: 0,
|
|
model,
|
|
source: 'quota_check',
|
|
})
|
|
const messages: MessageParam[] = [{ role: 'user', content: 'quota' }]
|
|
const betas = getModelBetas(model)
|
|
return anthropic.beta.messages
|
|
.create({
|
|
model,
|
|
max_tokens: 1,
|
|
messages,
|
|
metadata: getAPIMetadata(),
|
|
...(betas.length > 0 ? { betas } : {}),
|
|
})
|
|
.asResponse()
|
|
}
|
|
|
|
export async function checkQuotaStatus(): Promise<void> {
|
|
// Skip network requests if nonessential traffic is disabled
|
|
if (isEssentialTrafficOnly()) {
|
|
return
|
|
}
|
|
|
|
// Check if we should process rate limits (real subscriber or mock testing)
|
|
if (!shouldProcessRateLimits(isClaudeAISubscriber())) {
|
|
return
|
|
}
|
|
|
|
// In non-interactive mode (-p), the real query follows immediately and
|
|
// extractQuotaStatusFromHeaders() will update limits from its response
|
|
// headers (claude.ts), so skip this pre-check API call.
|
|
if (getIsNonInteractiveSession()) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
// Make a minimal request to check quota
|
|
const raw = await makeTestQuery()
|
|
|
|
// Update limits based on the response
|
|
extractQuotaStatusFromHeaders(raw.headers)
|
|
} catch (error) {
|
|
if (error instanceof APIError) {
|
|
extractQuotaStatusFromError(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if early warning should be triggered based on surpassed-threshold header.
|
|
* Returns ClaudeAILimits if a threshold was surpassed, null otherwise.
|
|
*/
|
|
function getHeaderBasedEarlyWarning(
|
|
headers: globalThis.Headers,
|
|
unifiedRateLimitFallbackAvailable: boolean,
|
|
): ClaudeAILimits | null {
|
|
// Check each claim type for surpassed threshold header
|
|
for (const [claimAbbrev, rateLimitType] of Object.entries(
|
|
EARLY_WARNING_CLAIM_MAP,
|
|
)) {
|
|
const surpassedThreshold = headers.get(
|
|
`anthropic-ratelimit-unified-${claimAbbrev}-surpassed-threshold`,
|
|
)
|
|
|
|
// If threshold header is present, user has crossed a warning threshold
|
|
if (surpassedThreshold !== null) {
|
|
const utilizationHeader = headers.get(
|
|
`anthropic-ratelimit-unified-${claimAbbrev}-utilization`,
|
|
)
|
|
const resetHeader = headers.get(
|
|
`anthropic-ratelimit-unified-${claimAbbrev}-reset`,
|
|
)
|
|
|
|
const utilization = utilizationHeader
|
|
? Number(utilizationHeader)
|
|
: undefined
|
|
const resetsAt = resetHeader ? Number(resetHeader) : undefined
|
|
|
|
return {
|
|
status: 'allowed_warning',
|
|
resetsAt,
|
|
rateLimitType: rateLimitType as RateLimitType,
|
|
utilization,
|
|
unifiedRateLimitFallbackAvailable,
|
|
isUsingOverage: false,
|
|
surpassedThreshold: Number(surpassedThreshold),
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Check if time-relative early warning should be triggered for a rate limit type.
|
|
* Fallback when server doesn't send surpassed-threshold header.
|
|
* Returns ClaudeAILimits if thresholds are exceeded, null otherwise.
|
|
*/
|
|
function getTimeRelativeEarlyWarning(
|
|
headers: globalThis.Headers,
|
|
config: EarlyWarningConfig,
|
|
unifiedRateLimitFallbackAvailable: boolean,
|
|
): ClaudeAILimits | null {
|
|
const { rateLimitType, claimAbbrev, windowSeconds, thresholds } = config
|
|
|
|
const utilizationHeader = headers.get(
|
|
`anthropic-ratelimit-unified-${claimAbbrev}-utilization`,
|
|
)
|
|
const resetHeader = headers.get(
|
|
`anthropic-ratelimit-unified-${claimAbbrev}-reset`,
|
|
)
|
|
|
|
if (utilizationHeader === null || resetHeader === null) {
|
|
return null
|
|
}
|
|
|
|
const utilization = Number(utilizationHeader)
|
|
const resetsAt = Number(resetHeader)
|
|
const timeProgress = computeTimeProgress(resetsAt, windowSeconds)
|
|
|
|
// Check if any threshold is exceeded: high usage early in the window
|
|
const shouldWarn = thresholds.some(
|
|
t => utilization >= t.utilization && timeProgress <= t.timePct,
|
|
)
|
|
|
|
if (!shouldWarn) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
status: 'allowed_warning',
|
|
resetsAt,
|
|
rateLimitType,
|
|
utilization,
|
|
unifiedRateLimitFallbackAvailable,
|
|
isUsingOverage: false,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get early warning limits using header-based detection with time-relative fallback.
|
|
* 1. First checks for surpassed-threshold header (new server-side approach)
|
|
* 2. Falls back to time-relative thresholds (client-side calculation)
|
|
*/
|
|
function getEarlyWarningFromHeaders(
|
|
headers: globalThis.Headers,
|
|
unifiedRateLimitFallbackAvailable: boolean,
|
|
): ClaudeAILimits | null {
|
|
// Try header-based detection first (preferred when API sends the header)
|
|
const headerBasedWarning = getHeaderBasedEarlyWarning(
|
|
headers,
|
|
unifiedRateLimitFallbackAvailable,
|
|
)
|
|
if (headerBasedWarning) {
|
|
return headerBasedWarning
|
|
}
|
|
|
|
// Fallback: Use time-relative thresholds (client-side calculation)
|
|
// This catches users burning quota faster than sustainable
|
|
for (const config of EARLY_WARNING_CONFIGS) {
|
|
const timeRelativeWarning = getTimeRelativeEarlyWarning(
|
|
headers,
|
|
config,
|
|
unifiedRateLimitFallbackAvailable,
|
|
)
|
|
if (timeRelativeWarning) {
|
|
return timeRelativeWarning
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function computeNewLimitsFromHeaders(
|
|
headers: globalThis.Headers,
|
|
): ClaudeAILimits {
|
|
const status =
|
|
(headers.get('anthropic-ratelimit-unified-status') as QuotaStatus) ||
|
|
'allowed'
|
|
const resetsAtHeader = headers.get('anthropic-ratelimit-unified-reset')
|
|
const resetsAt = resetsAtHeader ? Number(resetsAtHeader) : undefined
|
|
const unifiedRateLimitFallbackAvailable =
|
|
headers.get('anthropic-ratelimit-unified-fallback') === 'available'
|
|
|
|
// Headers for rate limit type and overage support
|
|
const rateLimitType = headers.get(
|
|
'anthropic-ratelimit-unified-representative-claim',
|
|
) as RateLimitType | null
|
|
const overageStatus = headers.get(
|
|
'anthropic-ratelimit-unified-overage-status',
|
|
) as QuotaStatus | null
|
|
const overageResetsAtHeader = headers.get(
|
|
'anthropic-ratelimit-unified-overage-reset',
|
|
)
|
|
const overageResetsAt = overageResetsAtHeader
|
|
? Number(overageResetsAtHeader)
|
|
: undefined
|
|
|
|
// Reason why overage is disabled (spending cap or wallet empty)
|
|
const overageDisabledReason = headers.get(
|
|
'anthropic-ratelimit-unified-overage-disabled-reason',
|
|
) as OverageDisabledReason | null
|
|
|
|
// Determine if we're using overage (standard limits rejected but overage allowed)
|
|
const isUsingOverage =
|
|
status === 'rejected' &&
|
|
(overageStatus === 'allowed' || overageStatus === 'allowed_warning')
|
|
|
|
// Check for early warning based on surpassed-threshold header
|
|
// If status is allowed/allowed_warning and we find a surpassed threshold, show warning
|
|
let finalStatus: QuotaStatus = status
|
|
if (status === 'allowed' || status === 'allowed_warning') {
|
|
const earlyWarning = getEarlyWarningFromHeaders(
|
|
headers,
|
|
unifiedRateLimitFallbackAvailable,
|
|
)
|
|
if (earlyWarning) {
|
|
return earlyWarning
|
|
}
|
|
// No early warning threshold surpassed
|
|
finalStatus = 'allowed'
|
|
}
|
|
|
|
return {
|
|
status: finalStatus,
|
|
resetsAt,
|
|
unifiedRateLimitFallbackAvailable,
|
|
...(rateLimitType && { rateLimitType }),
|
|
...(overageStatus && { overageStatus }),
|
|
...(overageResetsAt && { overageResetsAt }),
|
|
...(overageDisabledReason && { overageDisabledReason }),
|
|
isUsingOverage,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cache the extra usage disabled reason from API headers.
|
|
*/
|
|
function cacheExtraUsageDisabledReason(headers: globalThis.Headers): void {
|
|
// A null reason means extra usage is enabled (no disabled reason header)
|
|
const reason =
|
|
headers.get('anthropic-ratelimit-unified-overage-disabled-reason') ?? null
|
|
const cached = getGlobalConfig().cachedExtraUsageDisabledReason
|
|
if (cached !== reason) {
|
|
saveGlobalConfig(current => ({
|
|
...current,
|
|
cachedExtraUsageDisabledReason: reason,
|
|
}))
|
|
}
|
|
}
|
|
|
|
export function extractQuotaStatusFromHeaders(
|
|
headers: globalThis.Headers,
|
|
): void {
|
|
// Check if we need to process rate limits
|
|
const isSubscriber = isClaudeAISubscriber()
|
|
|
|
if (!shouldProcessRateLimits(isSubscriber)) {
|
|
// If we have any rate limit state, clear it
|
|
rawUtilization = {}
|
|
updateProviderBuckets('anthropic', [])
|
|
if (currentLimits.status !== 'allowed' || currentLimits.resetsAt) {
|
|
const defaultLimits: ClaudeAILimits = {
|
|
status: 'allowed',
|
|
unifiedRateLimitFallbackAvailable: false,
|
|
isUsingOverage: false,
|
|
}
|
|
emitStatusChange(defaultLimits)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Process headers (applies mocks from /mock-limits command if active)
|
|
const headersToUse = processRateLimitHeaders(headers)
|
|
rawUtilization = extractRawUtilization(headersToUse)
|
|
updateProviderBuckets(
|
|
'anthropic',
|
|
anthropicAdapter.parseHeaders(headersToUse),
|
|
)
|
|
const newLimits = computeNewLimitsFromHeaders(headersToUse)
|
|
|
|
// Cache extra usage status (persists across sessions)
|
|
cacheExtraUsageDisabledReason(headersToUse)
|
|
|
|
if (!isEqual(currentLimits, newLimits)) {
|
|
emitStatusChange(newLimits)
|
|
}
|
|
}
|
|
|
|
export function extractQuotaStatusFromError(error: APIError): void {
|
|
if (
|
|
!shouldProcessRateLimits(isClaudeAISubscriber()) ||
|
|
error.status !== 429
|
|
) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
let newLimits = { ...currentLimits }
|
|
if (error.headers) {
|
|
// Process headers (applies mocks from /mock-limits command if active)
|
|
const headersToUse = processRateLimitHeaders(error.headers)
|
|
rawUtilization = extractRawUtilization(headersToUse)
|
|
updateProviderBuckets(
|
|
'anthropic',
|
|
anthropicAdapter.parseHeaders(headersToUse),
|
|
)
|
|
newLimits = computeNewLimitsFromHeaders(headersToUse)
|
|
|
|
// Cache extra usage status (persists across sessions)
|
|
cacheExtraUsageDisabledReason(headersToUse)
|
|
}
|
|
// For errors, always set status to rejected even if headers are not present.
|
|
newLimits.status = 'rejected'
|
|
|
|
if (!isEqual(currentLimits, newLimits)) {
|
|
emitStatusChange(newLimits)
|
|
}
|
|
} catch (e) {
|
|
logError(e as Error)
|
|
}
|
|
}
|