3.10 API 通信 --- 多 Provider 客户端、多层重试引擎与错误恢复体系
对应原书 :第3章 3.4节"查询生命周期"中的 API 调用环节、第10章(附录)中的 CLI 双边架构、阶段十一(错误处理)的 API 重试与恢复
辅助源码 :
services/api/目录(20个文件)重点关注:多 Provider 客户端创建、withRetry AsyncGenerator 重试引擎、错误分类与恢复、流式 vs 非流式双路径、Prompt Cache Break 检测、会话持久化、Gateway 检测
1. 导语:API 通信层的定位
在 Claude Code 的架构中,services/api/ 目录位于整个调用链的最底层------它是系统与 Anthropic API(及其他 Provider)交互的唯一通道。查询引擎(QueryEngine)不直接调用 API,而是通过 claude.ts 中的 queryModelWithStreaming() / queryModelWithoutStreaming() 抽象函数完成 LLM 调用。这个目录同时承担了客户端管理、认证、重试、错误处理、日志遥测、缓存检测、会话持久化、文件传输、配置拉取九大职责,是整个系统与外部世界对话的"外交官"。
为什么一个 API 调用层需要 20 个文件?
因为 Claude Code 不只是"调一次 Anthropic API"那么简单。它需要:
- 支持 4 种 Provider(Direct API / AWS Bedrock / Azure Foundry / Vertex AI)
- 在 OAuth、API Key、Bearer Token、Azure AD、Google Auth 间灵活切换
- 处理 529 过载时的模型降级
- 在超长等待中保持心跳(持久重试模式)
- 检测 Prompt Cache 中断并归因
- 对 20+ 种错误类型给出用户友好的提示
- 支持流式和非流式双模式
- 将每一次 API 调用的详细信息记录到遥测系统
2. services/api/ 全景:20 文件架构
services/api/
├── client.ts # 多 Provider 客户端创建(390 行)
├── claude.ts # LLM API 调用核心(3419 行)⭐最大
├── withRetry.ts # AsyncGenerator 重试引擎(823 行)
├── errors.ts # 错误分类与消息转换(1208 行)
├── errorUtils.ts # SSL/连接错误工具(261 行)
├── logging.ts # API 日志与遥测(789 行)
├── promptCacheBreakDetection.ts # Cache Break 两阶段检测(728 行)
├── sessionIngress.ts # 远程会话日志持久化(515 行)
├── filesApi.ts # 文件上传下载(749 行)
├── bootstrap.ts # 启动配置拉取(142 行)
├── grove.ts # Grove 通知系统(358 行)
├── emptyUsage.ts # Usage 零值常量(22 行)
├── usage.ts # 用量查询(64 行)
├── adminRequests.ts # 管理员请求
├── dumpPrompts.ts # Prompt 导出
├── firstTokenDate.ts # 首 Token 日期
├── metricsOptOut.ts # 指标退出
├── overageCreditGrant.ts # 超额信用授权
├── referral.ts # 推荐
└── ultrareviewQuota.ts # UltraReview 配额
职责-大小矩阵
| 文件 | 行数 | 核心职责 | 关键设计 |
|---|---|---|---|
| claude.ts | 3419 | LLM 调用、流式/非流式、Usage 累积、Cache 控制 | AsyncGenerator + 累积式 Usage |
| errors.ts | 1208 | 20+ 错误分类、900+ 行错误→消息转换 | 穷举式匹配 + 用户友好文案 |
| withRetry.ts | 823 | AsyncGenerator 重试引擎、退避、529 特殊处理 | yield 通信 + 持久重试 |
| logging.ts | 789 | 请求/错误/成功日志、Gateway 检测、OTLP | 结构化日志 + 多网关指纹 |
| filesApi.ts | 749 | 文件上传下载、并发控制 | parallelWithLimit + cursor 分页 |
| promptCacheBreakDetection.ts | 728 | 两阶段 Cache Break 检测与归因 | 12 维状态追踪 |
| sessionIngress.ts | 515 | 会话日志持久化、乐观并发控制 | Last-Uuid header 冲突恢复 |
| client.ts | 390 | 多 Provider 客户端创建 | 工厂函数 + 环境变量路由 |
| grove.ts | 358 | 隐私政策通知 | 缓存优先非阻塞检查 |
3. 多 Provider 客户端架构:四路径共存
3.1 入口:getAnthropicClient() 工厂函数
typescript
// client.ts --- 核心工厂函数
export function getAnthropicClient(): Anthropic {
if (process.env.CLAUDE_CODE_USE_BEDROCK) {
return getBedrockClient() // AWS Bedrock
}
if (process.env.CLAUDE_CODE_USE_FOUNDRY) {
return getFoundryClient() // Azure Foundry
}
if (process.env.CLAUDE_CODE_USE_VERTEX) {
return getVertexClient() // Google Vertex AI
}
return getDirectClient() // Anthropic Direct API (默认)
}
四个环境变量决定走哪条路径,优先级按检查顺序排列。这是典型的"策略模式"实现------每种 Provider 返回一个同样实现了 SDK Anthropic 接口的客户端。
3.2 四条路径对比
| 路径 | 环境变量 | 认证方式 | 额外配置 |
|---|---|---|---|
| Direct API | 默认 | OAuth → API Key 退化 | ANTHROPIC_BASE_URL 代理 |
| AWS Bedrock | CLAUDE_CODE_USE_BEDROCK |
AWS Credential (Bearer token injected) | ANTHROPIC_AUTH_TOKEN / CLAUDE_CODE_BEDROCK_BASE_URL / AWS SDK credential chain |
| Azure Foundry | CLAUDE_CODE_USE_FOUNDRY |
Azure AD (DefaultAzureCredential) → API Key 退化 |
ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL |
| Google Vertex | CLAUDE_CODE_USE_VERTEX |
Google Auth (GoogleAuth) → API Key 退化 |
CLOUD_ML_REGION (default: us-east5) |
3.3 共通构造:buildFetch() --- 请求 ID 注入
typescript
// client.ts
export const CLIENT_REQUEST_ID_HEADER = 'x-client-request-id'
function buildFetch(): typeof globalThis.fetch {
const originalFetch = globalThis.fetch
return async (input, init) => {
const headers = new Headers(init?.headers)
if (!headers.has(CLIENT_REQUEST_ID_HEADER)) {
headers.set(CLIENT_REQUEST_ID_HEADER, randomUUID())
}
return originalFetch(input, { ...init, headers })
}
}
每个 API 请求自动携带 x-client-request-id,用于:
- 超时场景 下与服务器日志关联(
logAPIError中的clientRequestId字段) - 重复请求去重(幂等性保护)
3.4 SDK 日志器:createStderrLogger()
typescript
function createStderrLogger(): Logger {
return {
debug: (msg: string) => console.error(`[Anthropic SDK DEBUG] ${msg}`),
warn: (msg: string) => console.error(`[Anthropic SDK WARN] ${msg}`),
error: (msg: string) => console.error(`[Anthropic SDK ERROR] ${msg}`),
}
}
使用 console.error 而非 process.stderr.write,确保在 REPL 环境下不会干扰终端渲染。
3.5 自定义 Headers:ANTHROPIC_CUSTOM_HEADERS
typescript
function getCustomHeaders(): Record<string, string> {
const raw = process.env.ANTHROPIC_CUSTOM_HEADERS
if (!raw) return {}
return raw.split('\n').reduce((acc, line) => {
const colonIndex = line.indexOf(':')
if (colonIndex === -1) return acc
const key = line.slice(0, colonIndex).trim()
const val = line.slice(colonIndex + 1).trim()
if (key) acc[key] = val
return acc
}, {} as Record<string, string>)
}
支持换行分隔、冒号分割的多 header 注入,常用于企业代理场景。这与 3.9 中 envUtils.ts 的环境变量解析模式一脉相承。
4. Auth 多层认证:从 OAuth 到 Vertex 的退化链
4.1 认证层次
Claude Code 的认证不是单一方式,而是一条优先退化链:
OAuth Token (订阅用户)
↓ 未配置
API Key (ANTHROPIC_API_KEY / apiKeyHelper)
↓ 未配置
Bearer Token (ANTHROPIC_AUTH_TOKEN)
↓ 未配置
Azure AD (DefaultAzureCredential) --- Foundry 专用
↓ 无法获取
Google Auth (GoogleAuth) --- Vertex 专用
↓ 无法获取
→ 报错:无可用认证方式
4.2 configureApiKeyHeaders() --- 认证注入核心
typescript
// client.ts
async function configureApiKeyHeaders(client: Anthropic): Promise<void> {
// 1. 检查并刷新 OAuth token
await checkAndRefreshOAuthTokenIfNeeded()
// 2. 如果 OAuth token 可用,注入 Bearer header
const oauthToken = getOAuthToken()
if (oauthToken) {
client.apiKey = 'placeholder' // SDK 要求 apiKey 不为空
client.defaultHeaders = {
...client.defaultHeaders,
Authorization: `Bearer ${oauthToken}`,
}
return
}
// 3. 尝试 API Key
const apiKey = await getApiKey()
if (apiKey) {
client.apiKey = apiKey
return
}
// 4. 尝试 Bearer token(用于 Bedrock 等场景)
const authToken = process.env.ANTHROPIC_AUTH_TOKEN
if (authToken) {
client.apiKey = 'placeholder'
client.defaultHeaders = {
...client.defaultHeaders,
Authorization: `Bearer ${authToken}`,
}
}
}
关键设计 :每次创建客户端时都调用 checkAndRefreshOAuthTokenIfNeeded(),确保 token 不过期。OAuth token 通过 ANTHROPIC_BASE_URL 的 /v1/oauth/token 端点刷新。
4.3 附加保护 Header
typescript
// client.ts
if (process.env.CLAUDE_CODE_ADDITIONAL_PROTECTION) {
client.defaultHeaders['anthropic-additional-protection'] =
process.env.CLAUDE_CODE_ADDITIONAL_PROTECTION
}
环境变量 CLAUDE_CODE_ADDITIONAL_PROTECTION 可启用额外的保护头,用于企业级安全网关场景。
4.4 disableKeepAlive() --- Stale Connection 防御
当检测到 ECONNRESET/EPIPE 错误时:
typescript
// withRetry.ts
function isStaleConnectionError(error: any): boolean {
const code = error?.cause?.code || error?.code
return code === 'ECONNRESET' || code === 'EPIPE'
}
// 触发时禁用 keep-alive
if (isStaleConnectionError(error)) {
disableKeepAlive() // 下次请求使用 Connection: close
}
5. withRetry:AsyncGenerator 重试引擎深度剖析
5.1 核心签名
typescript
// withRetry.ts
export async function* withRetry<T>(
operation: () => Promise<T>, // 要重试的操作
options: {
maxRetries?: number // DEFAULT_MAX_RETRIES = 10
source?: QuerySource // 查询源(决定 529 策略)
onRetry?: (error: any, attempt: number) => void // 重试回调
signal?: AbortSignal // 取消信号
isStreaming?: boolean // 流式模式影响超时
}
): AsyncGenerator<SystemAPIErrorMessage, T>
返回类型是关键:AsyncGenerator<SystemAPIErrorMessage, T> ------ 这意味着重试过程中可能 yield 系统消息 (如"API 暂时不可用,正在重试..."),最终 return 结果 T。
5.2 重试常量
| 常量 | 值 | 说明 |
|---|---|---|
DEFAULT_MAX_RETRIES |
10 | 默认最大重试次数 |
MAX_529_RETRIES |
3 | 529 过载最大重试次数(触发模型降级) |
BASE_DELAY_MS |
500 | 指数退避基础延迟 |
PERSISTENT_MAX_BACKOFF_MS |
300000 (5min) | 持久重试最大退避 |
PERSISTENT_RESET_CAP_MS |
21600000 (6h) | 持久重试总时间上限 |
HEARTBEAT_INTERVAL_MS |
30000 (30s) | 持久模式心跳间隔 |
SHORT_RETRY_THRESHOLD_MS |
20000 (20s) | Fast 模式短退避阈值 |
DEFAULT_FAST_MODE_FALLBACK_HOLD_MS |
1800000 (30min) | cooldown 持续时间 |
MIN_COOLDOWN_MS |
600000 (10min) | 最小 cooldown 时间 |
5.3 shouldRetry() --- 重试决策函数
typescript
function shouldRetry(error: any, options: RetryOptions): RetryDecision {
// 1. Mock 模式 → 不重试
if (isMockTestMode()) return { retry: false }
// 2. 持久重试模式 → 429 / 529 无条件重试
if (isPersistentRetryEnabled() && (status === 429 || status === 529)) {
return { retry: true, persistent: true }
}
// 3. CCR 模式 → 401 / 403 也重试(token 可能过期)
if (isCcrEnabled() && (status === 401 || status === 403)) {
return { retry: true }
}
// 4. overloaded_error → 重试(Anthropic API 特有)
if (errorType === 'overloaded_error') return { retry: true }
// 5. max_tokens 上下文溢出 → 重试并调整 maxtokens
if (isMaxTokensContextOverflow(error)) return { retry: true, adjustMaxTokens: true }
// 6. x-should-retry header → 按服务器指示
const shouldRetryHeader = error?.headers?.['x-should-retry']
if (shouldRetryHeader === 'true') return { retry: true }
// 7. 5xx 服务端错误 → 重试
if (status >= 500) return { retry: true }
// 8. Connection Error → 重试
if (isConnectionError(error)) return { retry: true }
// 9. 408 Request Timeout → 重试
if (status === 408) return { retry: true }
// 10. 409 Conflict → 重试(乐观锁冲突)
if (status === 409) return { retry: true }
// 11. 429 速率限制 → 重试(非持久模式也重试)
if (status === 429) return { retry: true }
// 12. 401 + OAuth token 未撤销 → 重试(刷新 token)
if (status === 401 && !isOAuthTokenRevokedError(error)) return { retry: true }
// 13. 403 → 不重试
return { retry: false }
}
设计亮点 :决策函数是纯逻辑的,不包含退避计算。退避延迟由 getRetryDelay() 单独处理,职责分离清晰。
5.4 自定义错误类
typescript
// withRetry.ts
class CannotRetryError extends Error {
constructor(message: string) {
super(message)
this.name = 'CannotRetryError'
}
}
class FallbackTriggeredError extends Error {
constructor(message: string, public readonly fallbackModel: string) {
super(message)
this.name = 'FallbackTriggeredError'
}
}
CannotRetryError:用于 403、invalid_api_key、prompt_too_long 等不可重试场景FallbackTriggeredError:529 连续 3 次后触发模型降级,携带降级目标模型名
6. 529 过载错误的特殊处理:模型降级与持久重试
6.1 前台查询源白名单
typescript
// withRetry.ts
const FOREGROUND_529_RETRY_SOURCES = new Set([
'repl_main_thread',
'sdk',
'agent:*', // 所有 agent 子任务
'compact',
'hook_agent',
'auto_mode',
'bash_classifier',
])
非前台查询源(摘要生成、标题提取等)在 529 时 立即放弃不重试,因为:
- 这些任务是"锦上添花"而非核心功能
- 放弃它们可以让 token 预算更充裕地分配给主任务
6.2 模型降级触发
typescript
// withRetry.ts
if (consecutive529s >= MAX_529_RETRIES) {
throw new FallbackTriggeredError(
`Model ${currentModel} overloaded, falling back to ${fallbackModel}`,
fallbackModel
)
}
MAX_529_RETRIES = 3:连续 3 次 529 后触发降级。降级目标由 get3PModelFallbackSuggestion() 确定(在 errors.ts 中)。
6.3 持久重试模式
typescript
// withRetry.ts
function isPersistentRetryEnabled(): boolean {
return (
!!process.env.CLAUDE_CODE_UNATTENDED_RETRY &&
featureFlagEnabled('UNATTENDED_RETRY')
)
}
环境变量 CLAUDE_CODE_UNATTENDED_RETRY 加上 GrowthBook feature flag UNATTENDED_RETRY 双重开关。
持久重试行为:
- 429 / 529 无限重试,不计数
- 最高退避 5 分钟(
PERSISTENT_MAX_BACKOFF_MS) - 上限 6 小时(
PERSISTENT_RESET_CAP_MS)后放弃 - 每 30 秒打印心跳到 stderr(
HEARTBEAT_INTERVAL_MS) - 心跳格式:
⏳ Waiting for API (attempt N, elapsed H:MM:SS)...
这个模式专为无人值守的长时间任务设计(如 CI/CD pipeline 中的自动代码生成),确保即使 API 临时过载也不会导致任务失败。
7. Fast 模式重试策略:短退避保持缓存,长退避触发 cooldown
Fast 模式是 Claude Code 的加速特性------通过简化的系统提示和乐观的缓存策略来加速响应。当 Fast 模式下的 API 调用遇到速率限制时,重试策略需要特殊处理:
typescript
// withRetry.ts
function getFastModeRetryStrategy(retryAfterMs: number): FastModeStrategy {
if (retryAfterMs < SHORT_RETRY_THRESHOLD_MS) {
// 短退避:保持 Fast 模式重试,保留缓存收益
return { strategy: 'retain_fast', cooldown: false }
}
// 长退避:触发 cooldown,切换到标准速度
// 因为长时间等待后缓存可能已失效
return {
strategy: 'cooldown',
cooldown: true,
cooldownDuration: Math.max(
MIN_COOLDOWN_MS,
Math.min(retryAfterMs, DEFAULT_FAST_MODE_FALLBACK_HOLD_MS)
)
}
}
决策逻辑:
| retry-after | < 20s | ≥ 20s |
|---|---|---|
| 策略 | 保持 Fast 模式 | cooldown 切换 |
| 理由 | 缓存仍有效 | 缓存可能过期 |
| cooldown 时长 | 0 | max(10min, min(retry-after, 30min)) |
8. 指数退避与 jitter 算法
typescript
// withRetry.ts
function getRetryDelay(attempt: number, response?: Response): number {
// 1. 优先使用 retry-after header
const retryAfter = response?.headers?.get('retry-after')
if (retryAfter) {
const seconds = parseInt(retryAfter, 10)
if (!isNaN(seconds)) return seconds * 1000
// 也支持 HTTP-date 格式(但不常见)
const dateMs = Date.parse(retryAfter)
if (!isNaN(dateMs)) return Math.max(0, dateMs - Date.now())
}
// 2. 没有 retry-after → 指数退避 + jitter
const baseMs = BASE_DELAY_MS * Math.pow(2, attempt - 1) // 指数增长
const jitter = Math.random() * 0.5 - 0.25 // ±25%
const delay = baseMs + baseMs * jitter
return Math.min(delay, 32_000) // 上限 32 秒
}
退避序列(近似,含 jitter):
| 尝试次数 | 基础延迟 | +25% jitter 范围 |
|---|---|---|
| attempt 1 | 500ms | 375ms ~ 625ms |
| attempt 2 | 1000ms | 750ms ~ 1250ms |
| attempt 3 | 2000ms | 1500ms ~ 2500ms |
| attempt 4 | 4000ms | 3000ms ~ 5000ms |
| ... | ... | ... |
| attempt 7+ | 32000ms (cap) | 24000ms ~ 32000ms |
9. Max Tokens 上下文溢出自动修复
typescript
// withRetry.ts
function parseMaxTokensContextOverflowError(error: any): {
currentTokens: number
maxTokens: number
limit: number
} | null {
const message = error?.error?.message || error?.message || ''
// 匹配:"input length and max_tokens (X) exceed context limit (Y)"
const match = message.match(
/input length .*?(\d+).*?exceed context limit.*?(\d+)/
)
if (!match) return null
return {
currentTokens: parseInt(match[1], 10),
limit: parseInt(match[2], 10),
maxTokens: parseInt(match[1], 10), // 从消息中提取
}
}
当 400 错误包含上下文溢出信息时,系统自动:
- 解析出当前 token 数和上下文限制
- 计算可用的 token 余量
- 动态设置
maxTokensOverride降低 max_tokens - 以调整后的参数重试
这是自动修复机制------不需要用户手动调整任何参数。
10. 错误分类系统:20+ 精细错误类型
10.1 classifyAPIError() 完整分类
typescript
// errors.ts
function classifyAPIError(error: any): APIErrorType {
// 1. 用户中断
if (error?.name === 'AbortError' || error?.code === 'ABORT_ERR')
return 'aborted'
// 2. 超时(API 侧 + 客户端侧)
if (isTimeoutError(error))
return 'api_timeout'
// 3. 重复 529 --- 模型降级已触发
if (isFallbackTriggered(error))
return 'repeated_529'
// 4. 529 过载
if (status === 529)
return 'server_overload'
// 5. 429 速率限制
if (status === 429)
return 'rate_limit'
// 6. Prompt 过长
if (isPromptTooLong(error))
return 'prompt_too_long'
// 7. PDF 过大
if (isPdfTooLarge(error))
return 'pdf_too_large'
// 8. 图片过大
if (isImageTooLarge(error))
return 'image_too_large'
// 9. tool_use/tool_result 不匹配
if (isToolUseMismatch(error))
return 'tool_use_mismatch'
// 10. 无效模型
if (isInvalidModel(error))
return 'invalid_model'
// 11. 信用余额不足
if (isCreditBalanceLow(error))
return 'credit_balance_low'
// 12. 无效 API Key
if (isInvalidApiKey(error))
return 'invalid_api_key'
// 13. Token 被撤销
if (isTokenRevoked(error))
return 'token_revoked'
// 14. OAuth 组织不允许
if (isOauthOrgNotAllowed(error))
return 'oauth_org_not_allowed'
// 15. Auth 错误(通用)
if (status === 401 || status === 403)
return 'auth_error'
// 16. Bedrock 模型访问拒绝
if (isBedrockModelAccessError(error))
return 'bedrock_model_access'
// 17. 服务器错误(5xx 非 529)
if (status >= 500)
return 'server_error'
// 18. 客户端错误(4xx 非已处理)
if (status >= 400)
return 'client_error'
// 19. SSL/TLS 证书错误
if (isSslError(error))
return 'ssl_cert_error'
// 20. 连接错误(DNS/TCP)
if (isConnectionError(error))
return 'connection_error'
// 21. 未知错误
return 'unknown'
}
10.2 设计原则
- 从具体到通用 :先检查最具体的错误(如
tool_use_mismatch),再检查通用的 HTTP 状态码 - 可操作性强:每种错误类型都对应一条具体的用户消息,不出现模糊的"出现了一个错误"提示
- Provider 感知 :Bedrock 专属错误(如
bedrock_model_access)与通用错误分开处理
11. getAssistantMessageFromError:900+ 行错误转消息
这是 services/api/ 目录中最大的单个函数(约 900 行),将各种 API 错误转换为用户友好的 AssistantMessage。
11.1 函数结构
typescript
// errors.ts
export function getAssistantMessageFromError(
error: any,
options: ErrorMessageOptions
): AssistantMessage {
const type = classifyAPIError(error)
switch (type) {
case 'aborted':
return abortMessage(options)
case 'api_timeout':
return timeoutMessage(error, options)
case 'repeated_529':
return repeated529Message(error, options)
case 'rate_limit':
return rateLimitMessage(error, options)
case 'prompt_too_long': {
const { inputTokens, outputTokens } = parsePromptTooLongTokenCounts(error)
const gap = getPromptTooLongTokenGap(error)
return promptTooLongMessage(inputTokens, outputTokens, gap, options)
}
case 'credit_balance_low':
return creditBalanceLowMessage(options)
case 'invalid_api_key':
return invalidApiKeyMessage(options)
case 'token_revoked':
return tokenRevokedMessage(options)
case 'oauth_org_not_allowed':
return oauthOrgNotAllowedMessage(options)
case 'auth_error':
return authErrorMessage(error, options)
case 'ssl_cert_error': {
const sslHint = getSSLErrorHint(error)
return sslCertErrorMessage(sslHint, options)
}
case 'connection_error': {
const connDetails = extractConnectionErrorDetails(error)
return connectionErrorMessage(connDetails, options)
}
case 'server_error':
return serverErrorMessage(error, options)
case 'bedrock_model_access':
return bedrockModelAccessMessage(error, options)
// ... 更多 case
default:
return unknownErrorMessage(error, options)
}
}
11.2 错误消息示例
| 错误类型 | 用户消息文案(简化) |
|---|---|
prompt_too_long |
"The prompt is too long (X tokens). Try using /compact or starting a new conversation." |
credit_balance_low |
"Your credit balance is too low. Add credits link" |
invalid_api_key |
"Invalid API key. Run /login or set ANTHROPIC_API_KEY." |
rate_limit |
"Rate limited. Retrying in X seconds..." |
server_overload |
"Server overloaded (529). Switching to fallback model..." |
ssl_cert_error |
"SSL error. If using a corporate proxy, set NODE_EXTRA_CA_CERTS." |
11.3 Prompt Too Long 的 Token 计数解析
typescript
// errors.ts
function parsePromptTooLongTokenCounts(error: any): {
inputTokens: number
outputTokens: number
} {
// 从 400 错误消息中解析
// "prompt is too long: X tokens > Y maximum"
const message = error?.error?.message || error?.message || ''
const match = message.match(/(\d+)\s+tokens?\s*[>≥]\s*(\d+)/)
return {
inputTokens: match ? parseInt(match[1], 10) : 0,
outputTokens: match ? parseInt(match[2], 10) : 0,
}
}
function getPromptTooLongTokenGap(error: any): number {
const { inputTokens, outputTokens } = parsePromptTooLongTokenCounts(error)
return inputTokens - outputTokens // 需要压缩的 token 数
}
11.4 第三方模型降级建议链
typescript
// errors.ts
function get3PModelFallbackSuggestion(model: string): string | null {
const fallbackMap: Record<string, string> = {
'claude-opus-4-6-20251101': 'claude-opus-4-1-20250805',
'claude-sonnet-4-6-20251101': 'claude-sonnet-4-5-20250929',
'claude-sonnet-4-5-20250929': 'claude-sonnet-4-20250514',
'claude-haiku-4-5-20251001': 'claude-haiku-3-5-20241022',
}
return fallbackMap[model] || null
}
降级遵循"大版本 → 小版本"原则:Opus 4.6 → 4.1、Sonnet 4.6 → 4.5 → 4、Haiku 4.5 → 3.5。
12. 流式 vs 非流式双路径
12.1 两条调用链
queryModelWithStreaming() [AsyncGenerator]
└── withRetry(() => executeStreamingRequest())
└── client.messages.stream() // SDK 流式 API
└── for await (event of stream)
└── yield chunk
└── updateUsage() // 累积式更新
queryModelWithoutStreaming() [Promise]
└── withRetry(() => executeNonStreamingRequest())
└── client.messages.create() // SDK 非流式 API
└── return { message, usage }
12.2 流式 Usage 累积
typescript
// claude.ts --- updateUsage()
function updateUsage(current: NonNullableUsage, delta: PartialUsage): NonNullableUsage {
// ⚠️ input 相关字段仅在 > 0 时更新
// 原因:message_delta 事件的 usage.input_tokens 可能为 0
// 如果直接覆盖,会丢失 message_start 中的值
if (delta.input_tokens > 0) {
current.input_tokens = delta.input_tokens
}
if (delta.cache_creation_input_tokens > 0) {
current.cache_creation_input_tokens = delta.cache_creation_input_tokens
}
if (delta.cache_read_input_tokens > 0) {
current.cache_read_input_tokens = delta.cache_read_input_tokens
}
// output 字段始终更新(累加)
current.output_tokens = delta.output_tokens
return current
}
Anthropic 流式 API 的 usage 是累积值 (不是增量),但 message_delta 事件的 input_tokens 字段为 0。> 0 的守卫条件防止了信息丢失。
12.3 NonNullableUsage 类型
typescript
// emptyUsage.ts
export const EMPTY_USAGE: NonNullableUsage = {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
server_tool_use: 0,
cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 0 },
service_tier: 'standard',
inference_geo: null,
iterations: 0,
speed: null,
}
共 10 个字段,涵盖 Anthropic API 的所有 usage 维度。其中 server_tool_use / cache_creation / service_tier / inference_geo / iterations / speed 是非标准扩展字段。
12.4 非流式限制
| 参数 | 值 | 说明 |
|---|---|---|
MAX_NON_STREAMING_TOKENS |
64,000 | 非流式最大 token 数 |
| 远程超时 | 120s | getNonstreamingFallbackTimeoutMs() |
| 本地超时 | 300s | 本地 Provider(Bedrock 等) |
| Thinking Budget 调整 | 自动降低 | adjustParamsForNonStreaming() |
13. Prompt Cache Break 检测:两阶段归因系统
13.1 检测系统架构
Phase 1 (调用前) Phase 2 (调用后)
┌──────────────────┐ ┌─────────────────────────┐
│ recordPromptState │ │ checkResponseForCacheBreak│
│ 记录 12 维状态 │ │ 验证 + 归因 │
└────────┬─────────┘ └──────────┬──────────────┘
│ │
PreviousState compare + diff
│ │
└─────────────────────────────────────┘
13.2 12 维状态追踪
typescript
// promptCacheBreakDetection.ts
type PreviousState = {
systemHash: string // 系统提示 hash
toolsHash: string // 工具定义 hash
cacheControlHash: string // cache_control 断点 hash
toolNames: string[] // 工具名列表
perToolHashes: Record<string, string> // 每个工具的 hash
systemCharCount: number // 系统消息字符数
model: string // 模型名
fastMode: boolean // Fast 模式状态
globalCacheStrategy: 'tool_based' | 'system_prompt' | 'none'
betas: string[] // Beta 功能列表
autoModeActive: boolean // 自动模式状态
isUsingOverage: boolean // Overage 模式
cachedMCEnabled: boolean // MCP 缓存
effortValue?: number // Effort 值
extraBodyHash?: string // 额外 body hash
}
13.3 PendingChanges 变化检测
typescript
type PendingChanges = {
systemChanged: boolean
toolsChanged: boolean
cacheControlChanged: boolean
toolNamesChanged: boolean
perToolHashesChanged: boolean
systemCharCountChanged: boolean
modelChanged: boolean
fastModeChanged: boolean
globalCacheStrategyChanged: boolean
betasChanged: boolean
autoModeActiveChanged: boolean
isUsingOverageChanged: boolean
cachedMCEnabledChanged: boolean
effortValueChanged: boolean
extraBodyHashChanged: boolean
}
13.4 归因算法
typescript
// promptCacheBreakDetection.ts
function checkResponseForCacheBreak(
response: Usage,
previous: PreviousState
): CacheBreakReport | null {
// 只有 cache_read 下降 ≥ 2000 token 才报告
if (cacheMissTokens < MIN_CACHE_MISS_TOKENS) return null
// 归因:逐一比较 12 个维度
const causes: string[] = []
if (changes.systemChanged) causes.push('system_prompt_changed')
if (changes.toolsChanged) causes.push('tools_definition_changed')
if (changes.toolNamesChanged) causes.push('tool_names_changed')
// ... 更多原因
if (causes.length === 0) {
// 全维度无变化但 cache miss → 可能是 TTL 过期
const is5mExpiry = elapsed < CACHE_TTL_5MIN_MS
const is1hExpiry = elapsed < CACHE_TTL_1HOUR_MS
if (is5mExpiry) causes.push('cache_ttl_5m_expired')
else if (is1hExpiry) causes.push('cache_ttl_1h_expired')
else causes.push('cache_ttl_unknown_expired')
}
return { cacheMissTokens, causes }
}
13.5 1h Cache TTL 资格判断
typescript
// claude.ts
function should1hCacheTTL(): boolean {
// 1. Provider 检查:Bedrock 环境变量直通
if (process.env.CLAUDE_CODE_USE_BEDROCK) return true
// 2. 用户资格检查:ant 用户或非 overage 订阅用户
if (isAntUser() || (isSubscriber() && !isUsingOverage())) return true
// 3. GrowthBook allowlist 模式匹配
if (gbAllowlistPatternMatch()) return true
return false
}
三层条件,按优先级检查。资格在 session 内 latched(锁定),防止 mid-session 翻转导致缓存行为不一致。
13.6 Cache 基线重置
typescript
// 在压缩后调用
notifyCompaction()
// 在手动删除缓存后调用
notifyCacheDeletion()
两个函数将内部 previous state 重置,确保后续检测从新的基线开始。
14. API 日志与遥测:Gateway 检测与 OTLP
14.1 日志体系
| 函数 | 调用时机 | 记录内容 |
|---|---|---|
logAPIQuery() |
请求前 | model / messagesLength / temperature / betas / permissionMode / querySource |
logAPIError() |
错误时 | errorType / gateway / clientRequestId / connectionDetails / teleportInfo |
logAPISuccess() |
成功时 | usage / duration / ttft / costUSD / gateway / cacheStrategy / contentLengths |
logAPISuccessAndDuration() |
成功时(补充) | duration 单独的 timing 日志 |
14.2 7 种 AI Gateway 检测
typescript
// logging.ts
function detectGateway(response: Response): GatewayInfo | null {
const headers = response.headers
// 按 header 指纹 + host 后缀双重识别
const gateways: GatewayFingerprint[] = [
{ name: 'litellm', header: 'x-litellm-version' },
{ name: 'helicone', header: 'helicone-id' },
{ name: 'portkey', header: 'x-portkey-trace-id' },
{ name: 'cloudflare-ai-gateway', header: 'cf-aig-cache-status' },
{ name: 'kong', header: 'x-kong-proxy-latency' },
{ name: 'braintrust', header: 'x-bt-request-id' },
{ name: 'databricks', hostSuffix: '.databricks.com' },
]
for (const gw of gateways) {
if (gw.header && headers.get(gw.header)) return { name: gw.name }
if (gw.hostSuffix && url.includes(gw.hostSuffix)) return { name: gw.name }
}
return null
}
14.3 OTLP 遥测事件
两种 OTLP 事件:
api_error:错误时触发,携带 errorType / gateway / statusCodeapi_request:成功时触发,携带 usage / duration / model / cost
14.4 Beta Tracing
typescript
// logging.ts
function extractBetaTracingData(response: any): BetaTracingData {
return {
modelOutput: response?.content?.[0]?.text?.slice(0, 500), // 截断
thinkingOutput: extractThinkingOutput(response),
hasToolCall: response?.content?.some(isToolUseBlock) ?? false,
}
}
Beta Tracing 记录模型输出和 thinking 输出的前 500 字符,用于产品分析。hasToolCall 用于统计工具调用率。
15. Session Ingress:乐观并发控制的会话持久化
15.1 架构
appendSessionLog()
└── Promise 链(per-session 串行)
└── sequential(sessionId, () => appendSessionLogImpl())
└── 乐观并发控制
├── 成功 → 保存返回的 UUID
└── 409 → 采纳服务器 UUID 重试
15.2 乐观并发控制实现
typescript
// sessionIngress.ts
async function appendSessionLogImpl(
sessionId: string,
events: SessionEvent[],
lastUuid: string | null
): Promise<string> {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(lastUuid ? { 'Last-Uuid': lastUuid } : {}), // CAS token
},
body: JSON.stringify({ events }),
})
// 成功:服务器返回新的 UUID
return response.headers.get('X-Uuid') || ''
} catch (error) {
if (error.status === 409) {
// 冲突:采纳服务器的 UUID
const serverUuid = error.headers?.['x-uuid']
if (serverUuid) {
// 重试:用服务器的 UUID 重新发送
return appendSessionLogImpl(sessionId, events, serverUuid)
}
}
throw error
}
}
关键设计 :Last-Uuid header 充当 CAS (Compare-And-Swap) token。客户端发送最后一次确认的 UUID,服务器检查是否匹配。不匹配返回 409,客户端采纳服务器的 X-Uuid 后重试。
15.3 Per-Session 串行写入
typescript
const sessionQueues = new Map<string, Promise<void>>()
function sequential(sessionId: string, fn: () => Promise<void>): Promise<void> {
const prev = sessionQueues.get(sessionId) || Promise.resolve()
const next = prev.then(fn).catch(() => {}) // 错误不阻塞后续
sessionQueues.set(sessionId, next)
return next
}
每个会话串行写入,防止并发写竞争。但不同会话之间是并行的。
15.4 Teleport Events 分页获取
typescript
// sessionIngress.ts
async function getTeleportEvents(sessionId: string): Promise<SessionEvent[]> {
const events: SessionEvent[] = []
let cursor: string | null = null
let pageCount = 0
const MAX_PAGES = 100
do {
const response = await fetch(
`/v2/sessions/${sessionId}/events?cursor=${cursor}&limit=100`
)
const data = await response.json()
events.push(...data.events)
cursor = data.next_cursor
pageCount++
} while (cursor && pageCount < MAX_PAGES)
return events
}
Cursor-based 分页,每页 100 条,上限 100 页(最多 10,000 条事件)。
16. Files API:并发限制的上传下载
16.1 下载:并行 + 并发限制
typescript
// filesApi.ts
async function downloadSessionFiles(
fileIds: string[],
outputDir: string
): Promise<DownloadResult[]> {
return parallelWithLimit(
fileIds.map(id => () => downloadAndSaveFile(id, outputDir)),
DEFAULT_CONCURRENCY // 默认 5
)
}
function parallelWithLimit<T>(
tasks: (() => Promise<T>)[],
limit: number
): Promise<T[]> {
const results: T[] = new Array(tasks.length)
let index = 0
async function worker() {
while (index < tasks.length) {
const currentIndex = index++
results[currentIndex] = await tasks[currentIndex]()
}
}
return Promise.all(Array.from({ length: limit }, () => worker()))
.then(() => results)
}
16.2 上传:Multipart + 500MB 上限
typescript
async function uploadFile(
filePath: string,
options?: UploadOptions
): Promise<UploadResult> {
const stat = await fs.stat(filePath)
if (stat.size > 500 * 1024 * 1024) {
throw new Error('File exceeds 500MB limit')
}
const form = new FormData()
form.append('file', createReadStream(filePath))
form.append('purpose', options?.purpose || 'user_data')
return fetch('/v1/files', { method: 'POST', body: form })
}
16.3 路径遍历防御
typescript
function buildDownloadPath(outputDir: string, filename: string): string {
const resolved = path.resolve(outputDir, filename)
if (!resolved.startsWith(path.resolve(outputDir))) {
throw new Error(`Path traversal detected: ${filename}`)
}
return resolved
}
防止文件名为 ../../../etc/passwd 的攻击。
17. Bootstrap API:启动时配置拉取
typescript
// bootstrap.ts
async function fetchBootstrapAPI(): Promise<BootstrapData | null> {
try {
const response = await withOAuth401Retry(() =>
fetch(`${API_BASE}/v1/bootstrap`)
)
if (!response.ok) return null
return response.json()
} catch {
return null // 启动失败不阻塞 CLI
}
}
async function fetchBootstrapData(): Promise<void> {
const data = await fetchBootstrapAPI()
if (!data) return
// 持久化到全局配置
setGlobalConfig({
clientDataCache: data.client_data,
additionalModelOptionsCache: data.additional_model_options,
})
}
设计原则 :启动时异步拉取,失败不阻塞 CLI 启动。OAuth 用户通过 withOAuth401Retry 包装,API Key 用户直接请求。
18. Grove 通知系统:隐私政策更新的非阻塞推送
18.1 非阻塞检测
typescript
// grove.ts
async function isQualifiedForGrove(): Promise<boolean> {
// 缓存优先:从 Statsig 检查
const cached = getGroveCache()
if (cached !== undefined) return cached
// 异步非阻塞获取
const config = await getGroveNoticeConfig()
if (!config) return false
// 检查用户是否已查看
const settings = await getGroveSettings()
if (settings.lastViewedAt >= config.publishedAt) return false
// 检查 grace period
const now = Date.now()
if (now < config.publishedAt + config.gracePeriod) return false
return true
}
18.2 非交互模式处理
typescript
function checkGroveForNonInteractive(): void {
if (!isQualifiedForGroveSync()) return
const config = getGroveConfigSync()
const now = Date.now()
if (now < config.publishedAt + config.gracePeriod) {
// Grace period: 显示信息消息,不阻塞
console.warn(config.infoMessage)
} else {
// 过期:退出并提示用户
console.error(config.expiredMessage)
process.exit(1)
}
}
设计意图:隐私政策更新是合规要求,不能忽略。但在交互模式中,通过 grace period 给用户缓冲时间;在 non-interactive 模式中,过期后强制退出确保合规。
19. 设计模式提炼
19.1 策略模式 --- 多 Provider
typescript
// 每次调用 getAnthropicClient() 返回特定 Provider 的客户端
// 但上层代码无需关心具体是哪个 Provider
const client = getAnthropicClient()
const response = await client.messages.create({ ... })
19.2 AsyncGenerator 控制反转 --- withRetry
typescript
// 调用方通过 for await 获取系统消息,最终获得结果
for await (const msg of withRetry(operation)) {
// msg 是 SystemAPIErrorMessage --- 可以展示给用户
console.log(msg.text)
}
// 循环结束后 result 可用
19.3 CAS 乐观并发 --- Session Ingress
客户端记录 lastUuid → 发送请求带 Last-Uuid header
├── 200: 匹配,更新 lastUuid
└── 409: 冲突,采纳服务器的 X-Uuid → 重试
19.4 累积式 Usage --- 流式 API
typescript
// input 字段使用 > 0 守卫,防止 message_delta 的 0 覆盖真实值
if (delta.input_tokens > 0) {
current.input_tokens = delta.input_tokens
}
// output 字段始终更新
current.output_tokens = delta.output_tokens
19.5 两阶段检测 --- Cache Break
Phase 1: recordPromptState() --- 在调用前快照
Phase 2: checkResponseForCacheBreak() --- 调用后验证并归因
20. 原书对照验证
| 书中描述 | 源码位置 | 验证结果 |
|---|---|---|
| "API 调用通过 Anthropic SDK 完成" | client.ts:getAnthropicClient() |
✅ 使用官方 @anthropic-ai/sdk,工厂函数创建 |
| "流式响应通过 AsyncGenerator 模式实现" | claude.ts:queryModelWithStreaming() |
✅ async function* 返回 AsyncGenerator |
| "重试机制包含指数退避" | withRetry.ts:getRetryDelay() |
✅ BASE_DELAY_MS * 2^(attempt-1) + 25% jitter |
| "API 错误有分类处理" | errors.ts:classifyAPIError() |
✅ 20+ 种错误类型 |
| "成本通过 Usage 对象追踪" | claude.ts:updateUsage() / emptyUsage.ts:EMPTY_USAGE |
✅ 10 字段 NonNullableUsage |
| "Prompt Cache 中断有检测机制" | promptCacheBreakDetection.ts |
✅ 两阶段 12 维检测 + 归因 |
| "支持多个云 Provider" | client.ts 四条路径 |
✅ Direct API / Bedrock / Foundry / Vertex |
| "认证有多层退化" | client.ts:configureApiKeyHeaders() |
✅ OAuth → API Key → Bearer → Azure AD → Google Auth |
| "Gateway 检测用于日志" | logging.ts:detectGateway() |
✅ 7 种网关 header 指纹 |
| "持久重试支持无人值守" | withRetry.ts:isPersistentRetryEnabled() |
✅ CLAUDE_CODE_UNATTENDED_RETRY + feature flag |
原书对 API 通信的直接描述较少 (主要集中在第3.4节"查询生命周期"),但源码的 services/api/ 模块远比书中描述丰富------20 个文件、约 9000 行代码,构成了完整的 API 通信基础设施。这些代码在原书第11章"错误处理"中也有涉及,构成了阶段十一的前置知识。
总结 :
services/api/是一个精密的外交层。它不仅仅是"调 API",而是一个集多 Provider 路由、多层认证、智能重试、错误恢复、缓存检测、会话持久化、遥测日志 于一体的完整通信系统。其中最精妙的设计是withRetry的 AsyncGenerator 模式------通过yield实现重试进度的外部可见性,使得长时间等待不再是黑盒。