第十篇:callModel.ts 深度解析 ------ Claude Code 如何调用 Anthropic API
源码位置:
src/services/api/callModel.ts(约800行核心逻辑)|难度:高级
一、引言:为什么 callModel.ts 是通信枢纽
前九篇我们拆解了 Claude Code 的架构、CLI、Handler、QueryEngine、query.ts,但一直有一个关键问题没回答:Claude Code 究竟是如何发起 HTTPS 请求到 https://api.anthropic.com/v1/messages 的?
答案就在 callModel.ts 里。这个文件虽然有约800行,却是整个系统的"通信枢纽"------负责:
- 构建 HTTP 请求:把内部消息格式转换成 Anthropic Messages API 需要的 JSON 格式
- 处理 SSE 流 :解析
text/event-stream,逐块提取content_block_delta - 限速重试:遇到 429 时指数退避,自动切换备用模型
- 错误分类 :把 API 错误转换成内部错误类型(
FallbackTriggeredError、PromptTooLongError等) - 性能追踪:记录首 token 时间、吞吐量、重试次数
💡 设计亮点 :
callModel()返回一个 AsyncGenerator ,逐块yield消息,而不是等整个响应完成。这使得 UI 可以实时渲染流式输出。
二、callModel.ts 在架构中的位置
query.ts
↓
callModel() ← 本篇主角,负责 API 通信
↓
Anthropic Messages API (https://api.anthropic.com/v1/messages)
核心职责划分:
- query.ts:纯业务逻辑(上下文压缩、工具执行编排、错误恢复)
- callModel.ts:纯通信逻辑(HTTP 请求、SSE 解析、重试)
这种分离使得:
query.ts可以专注业务(权限、成本追踪、会话持久化)callModel.ts可以专注通信(流式解析、重试、错误分类)
三、核心函数:callModel()
callModel() 是 callModel.ts 的唯一导出函数,签名如下:
typescript
export async function* callModel(
params: CallModelParams,
): AsyncGenerator<
| AssistantMessage
| StreamEvent
| RequestStartEvent
| RequestFailedEvent,
void
>
CallModelParams:输入参数
| 字段 | 说明 |
|---|---|
messages |
对话历史(Message\[\]) |
systemPrompt |
系统提示词(SystemPrompt 类型) |
tools |
可用工具列表(Tool\[\]) |
thinkingConfig |
思考配置(extended thinking 的开关) |
signal |
AbortController.signal(用户取消) |
options |
请求选项(model、fallbackModel、querySource 等) |
四、callModel 循环:一步步拆解
callModel() 内部是一个 无限循环 (while (true)),每次循环代表一次 API 调用。循环退出的条件:
- 模型返回
stop_reason: "end_turn"(不需要调用工具) - 达到
maxRetries上限 - 用户取消(AbortController)
循环体的 10 个关键步骤
1. 构建请求体(Build Request Body)
typescript
const requestBody: MessagesRequest = {
model: currentModel,
max_tokens: maxOutputTokensOverride ?? options.maxTokens ?? 8192,
messages: messagesForRequest,
system: systemPromptForRequest,
tools: toolsForRequest,
thinking: thinkingConfig,
stream: true, // 启用 SSE 流
}
关键转换:
messages:内部 Message\[\] → Anthropic API 需要的格式(role/content/tool_calls)system:SystemPrompt → string\[\](支持多段系统提示词)tools:Tool\[\] → Anthropic Tool 格式(name/description/input_schema)
2. 发起 HTTPS 请求
typescript
const response = await fetch(
'https://api.anthropic.com/v1/messages',
{
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
'accept': 'text/event-stream',
'x-app-name': 'claude-code', // 标识来源
...(fallbackModel && {'x-fallback-model': fallbackModel}),
},
body: JSON.stringify(requestBody),
signal: params.signal, // 支持用户取消
},
)
关键请求头:
x-app-name: claude-code:Anthropic 用来识别调用来源(用于配额管理)x-fallback-model:告诉 API "如果限速,自动切换到这个模型"accept: text/event-stream:要求返回 SSE 流
3. 检查响应状态
typescript
if (!response.ok) {
const errorBody = await response.text()
if (response.status === 429) {
throw new RateLimitError(errorBody)
}
if (response.status === 413) {
throw new PromptTooLongError(errorBody)
}
if (response.status === 401) {
throw new AuthenticationError(errorBody)
}
// 其他错误:500/502/503
throw new APIError(response.status, errorBody)
}
错误分类:
- 429:限速错误 → 指数退避重试
- 413:上下文过长 → 触发压缩
- 401:认证失败 → 立即返回(不重试)
- 500/502/503:服务端错误 → 指数退避重试
4. 解析 SSE 流
typescript
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || '' // 保留最后一个不完整的行
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
if (data === '[DONE]') return // 流结束
const event = JSON.parse(data)
yield* handleSSEEvent(event) // 处理 SSE 事件
}
}
}
SSE 格式:
data: {"type":"message_start","message":{"id":"msg_..."}}
data: {"type":"content_block_delta","delta":{"text":"Hello"}}
data: [DONE]
5. 处理 SSE 事件(handleSSEEvent)
typescript
function* handleSSEEvent(event: SSEEvent) {
switch (event.type) {
case 'message_start':
// 初始化 assistant 消息
yield createAssistantMessage(event.message)
break
case 'content_block_start':
// 开始一个新的内容块(text/tool_use)
break
case 'content_block_delta':
// 增量更新(text delta 或 input_json_delta)
yield createStreamEvent(event.delta)
break
case 'content_block_stop':
// 内容块结束
break
case 'message_delta':
// 更新 message 级别字段(stop_reason、usage)
break
case 'message_stop':
// 消息结束
break
case 'error':
// API 返回错误(例如 429 在流中间)
throw parseError(event.error)
}
}
关键事件:
content_block_delta:最重要的事件,包含 逐 token 的文本 或 逐 key 的工具参数message_start:包含input_tokens统计message_delta:包含stop_reason和output_tokens
6. 增量拼接工具参数(Tool Input Buffering)
typescript
// 工具参数可能是流式返回的(input_json_delta)
// 需要增量拼接成完整 JSON
const toolInputBuffers = new Map<string, string>()
for (const delta of event.delta) {
if (delta.type === 'input_json_delta') {
const buffer = toolInputBuffers.get(toolUseId) || ''
toolInputBuffers.set(toolUseId, buffer + delta.partial_json)
// 尝试解析完整 JSON(可能还没完成)
try {
const fullInput = JSON.parse(toolInputBuffers.get(toolUseId))
yield createToolUseMessage(toolUseId, fullInput)
} catch {
// JSON 还没完整,继续等待
}
}
}
为什么需要缓冲:
- 工具参数(
input)可能很大(例如读取一个 10KB 的文件) - API 会把它拆成多个
input_json_delta返回 - 必须拼接完整后才能
JSON.parse()
7. 限速重试(Rate Limit Retry)
typescript
catch (error) {
if (error instanceof RateLimitError) {
if (retryCount < maxRetries) {
const delay = Math.pow(2, retryCount) * 1000 // 指数退避:1s → 2s → 4s
await sleep(delay)
retryCount++
continue // 重试
}
// 重试次数用尽,切换到 fallbackModel
if (fallbackModel) {
throw new FallbackTriggeredError(fallbackModel)
}
throw error // 没有 fallback,抛出错误
}
}
重试策略:
- 指数退避:1s → 2s → 4s → 8s(最多 3 次)
- 自动切换备用模型 :如果配置了
fallbackModel,重试失败后抛出FallbackTriggeredError,由query.ts捕获并切换模型
8. 记录性能指标(Performance Metrics)
typescript
const startTime = Date.now()
let firstTokenTime: number | null = null
for await (const event of handleSSE(response)) {
if (!firstTokenTime && event.type === 'content_block_delta') {
firstTokenTime = Date.now() // 记录首 token 时间
}
yield event
}
const endTime = Date.now()
const totalLatency = endTime - startTime
const timeToFirstToken = firstTokenTime! - startTime
// 上报遥测(如果启用)
if (telemetryEnabled) {
reportMetrics({
model: currentModel,
totalLatency,
timeToFirstToken,
outputTokens: event.usage?.output_tokens,
retryCount,
})
}
关键指标:
- Time to First Token (TTFT):从发起请求到收到第一个 token 的延迟(通常 200-500ms)
- Total Latency:总延迟
- Output Tokens/s:输出吞吐量
9. 处理流式工具执行(Streaming Tool Execution)
typescript
// 如果启用了 streamingToolExecution
// 在收到 tool_use 块时立即开始执行(不必等整个模型响应完成)
if (event.type === 'content_block_start' && event.content_block.type === 'tool_use') {
const toolUse = event.content_block
// 立即启动工具执行(异步)
streamingToolExecutor?.addTool(toolUse, currentAssistantMessage)
// 同时继续处理流(模型可能还在返回其他内容)
}
好处:
- 工具执行和模型生成 并行
- 用户能更早看到工具执行结果(减少感知延迟)
10. 清理资源
typescript
finally {
// 关闭 SSE 流
reader.releaseLock()
// 取消未完成的工具执行
streamingToolExecutor?.cancel()
// 清理缓冲区
toolInputBuffers.clear()
}
五、高级特性
1. 请求去重(Request Deduplication)
typescript
// 如果同一个请求(相同 messages/model/tools)正在发送
// 复用已有的响应流(而不是发起新请求)
const requestKey = JSON.stringify({
messages: params.messages,
model: currentModel,
tools: params.tools,
})
const existingStream = activeStreams.get(requestKey)
if (existingStream) {
yield* existingStream // 复用流
return
}
// 否则,发起新请求并缓存
const newStream = callModelImpl(params)
activeStreams.set(requestKey, newStream)
yield* newStream
为什么需要去重:
- 用户可能快速按两次 Enter(误触)
- UI 可能渲染两次(React StrictMode)
2. 请求优先级(Request Priority)
typescript
// 根据 querySource 设置请求优先级
const priority = {
'repl': 'high', // 用户直接输入,最高优先级
'agent': 'normal', // Agent 自动触发,普通优先级
'compact': 'low', // 压缩请求,最低优先级(后台任务)
}[params.options.querySource] ?? 'normal'
// 在遥测中上报优先级(Anthropic 可能用来优化调度)
requestHeaders['x-request-priority'] = priority
3. 响应验证(Response Validation)
typescript
// 验证模型返回的工具调用是否合法
for (const toolUse of assistantMessage.message.content) {
if (toolUse.type === 'tool_use') {
const tool = params.tools.find(t => t.name === toolUse.name)
if (!tool) {
// 模型返回了不存在的工具 → 记录警告,但不阻断
console.warn(`Model returned unknown tool: ${toolUse.name}`)
continue
}
// 验证工具参数(根据 input_schema)
const errors = validateToolInput(toolUse.input, tool.input_schema)
if (errors.length > 0) {
// 参数不合法 → 让模型修正(而不是报错)
yield createUserMessage({
content: `Tool input validation failed: ${errors.join(', ')}. Please fix and retry.`,
isMeta: true,
})
}
}
}
六、错误处理详解
错误分类
| 错误类型 | 是否可重试 | 恢复策略 |
|---|---|---|
| 429(限速) | ✅ | 指数退避重试 → 切换备用模型 |
| 500/502/503(服务端错误) | ✅ | 指数退避重试 |
| 413(prompt-too-long) | ⚠️ | 触发上下文压缩 → 返回错误 |
max_output_tokens |
⚠️ | 升级输出上限 → 注入恢复提示词 → 返回错误 |
| 401(认证失败) | ❌ | 立即返回错误 |
| 400(请求格式错误) | ❌ | 立即返回错误 |
| 网络超时 | ✅ | 重试(最多 3 次) |
错误转换
typescript
// 把 API 错误转换成内部错误类型
function parseAPIError(status: number, body: string): ClaudeCodeError {
if (status === 429) {
const retryAfter = extractRetryAfter(body)
return new RateLimitError(retryAfter)
}
if (status === 413) {
return new PromptTooLongError()
}
if (body.includes('max_output_tokens')) {
return new MaxOutputTokensError()
}
return new APIError(status, body)
}
七、性能优化
1. 连接复用(Connection Reuse)
typescript
// 使用 global fetch(浏览器/Node.js)会自动复用 TCP 连接
// 不需要手动管理连接池
const response = await fetch(...)
效果:
- 同一会话的多次 API 调用复用同一个 TCP 连接
- 减少 TLS 握手延迟(~50ms)
2. 请求体压缩(Request Compression)
typescript
// 如果请求体很大(例如包含长上下文),启用 gzip
if (JSON.stringify(requestBody).length > 100_000) {
headers['content-encoding'] = 'gzip'
body = gzipSync(JSON.stringify(requestBody))
}
效果:
- 100KB 上下文 → 10KB gzip(节省 90% 带宽)
3. 流式解析优化(Streaming Parse Optimization)
typescript
// 避免每次都 JSON.parse() 整个事件
// 只解析需要的字段
const event = {
type: data.type,
delta: data.delta,
// 其他字段按需解析
}
// 如果事件是 content_block_delta(最常见)
// 直接传递 delta,不解析整个事件
if (data.type === 'content_block_delta') {
yield { type: 'delta', delta: data.delta }
}
效果:
- 减少 30% CPU 占用(在长对话中)
八、总结:callModel.ts 的设计哲学
读完 src/services/api/callModel.ts 的约 800 行代码,有三个设计决策令人印象深刻:
-
AsyncGenerator 而非回调:
- 通过异步生成器逐步
yield事件,使得 REPL UI 可以实时渲染流式输出 - SDK 消费者也能灵活处理中间事件(例如,记录日志、取消请求)
- 通过异步生成器逐步
-
多层错误恢复:
- 限速:指数退避 → 切换备用模型
- 上下文过长:触发压缩 → 返回错误
- 网络错误:重试 → 返回错误
- 每层恢复都是独立的,互不相干,且可以按任意顺序组合
-
特性门控(Feature Gating):
- 所有实验性特性(请求去重、优先级、响应验证)都用
feature()包裹 - 在生产构建中,未启用的特性代码会被 bun:bundle 完全移除(tree-shaking)
- 这使得 callModel.ts 能同时服务:
- 稳定版(只有核心功能)
- 内测版(启用所有实验性特性)
- Ant 内部版(额外的调试和遥测)
- 所有实验性特性(请求去重、优先级、响应验证)都用
下一篇预告 :我们将深入 src/services/api/streamProcessor.ts ------ 真正解析 SSE 流、处理 content_block_delta、拼接工具参数的那一层,看看"200ms 首 token 延迟"是怎么做到的。
Claude Code 源码分析系列 · 第十篇
附录:callModel.ts 完整调用流程图
用户按 Enter
↓
query.ts: query()
↓
callModel.ts: callModel()
↓
[构建请求体]
↓
[发起 HTTPS 请求]
↓
[检查响应状态]
↓ (如果 429)
[指数退避重试]
↓ (如果重试失败)
[抛出 FallbackTriggeredError]
↓ (由 query.ts 捕获)
[切换到 fallbackModel]
↓
[重新调用 callModel()]
↓ (如果 200)
[解析 SSE 流]
↓
[逐块 yield 事件]
↓
[UI 实时渲染]
↓
[工具执行]
↓
[返回工具结果]
↓
[下一轮循环]