mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
* refactor: 创建 @anthropic-ai/model-provider 包骨架与类型定义
- 新建 workspace 包 packages/@anthropic-ai/model-provider
- 定义 ModelProviderHooks 接口(依赖注入:分析、成本、日志等)
- 定义 ClientFactories 接口(Anthropic/OpenAI/Gemini/Grok 客户端工厂)
- 搬入核心类型:Message 体系、NonNullableUsage、EMPTY_USAGE、SystemPrompt、错误常量
- 主项目 src/types/message.ts 等改为 re-export,保持向后兼容
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: 提升 OpenAI 转换器和模型映射到 model-provider 包
- 搬入 OpenAI 消息转换(convertMessages)、工具转换(convertTools)、流适配(streamAdapter)
- 搬入 OpenAI 和 Grok 模型映射(resolveOpenAIModel、resolveGrokModel)
- 主项目文件改为 thin re-export proxy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: 搬入 Gemini 兼容层到 model-provider 包
- 搬入 Gemini 类型定义、消息转换、工具转换、流适配、模型映射
- 主项目 gemini/ 目录下文件改为 thin re-export proxy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: 搬入 errorUtils 并迁移消费者导入到 model-provider
- 搬入 formatAPIError、extractConnectionErrorDetails 等 errorUtils
- 迁移 10 个消费者文件直接从 @anthropic-ai/model-provider 导入
- 更新 emptyUsage、sdkUtilityTypes、systemPromptType 为 re-export proxy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: 添加 agent-loop 绘图
* Revert "feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)"
This reverts commit e458d6391d.
* docs: 添加简化版 agent loop
* fix: 修复 n 快捷键导致关闭的问题
* fix: 修复 node 下 ws 没打包问题
* docs: 修复链接
* test: 添加测试支持
* fix: 修复类型问题(#267) (#271)
* fix: 修复 Bun 的 polyfill 问题
* fix: 类型修复完成
* feat: 统一所有包的类型文件
* fix: 修复构建问题
* test: 修复类型校验 (#279)
* fix: 修复 Bun 的 polyfill 问题
* fix: 类型修复完成
* feat: 统一所有包的类型文件
* fix: 修复构建问题
* fix(remote-control): harden self-hosted session flows (#278)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
* docs: update contributors
* build: 新增 vite 构建流程
* feat: 添加环境变量支持以覆盖 max_tokens 设置
* feat(langfuse): LLM generation 记录工具定义
将 Anthropic 格式的工具定义转换为 Langfuse 兼容的 OpenAI 格式,
并在 generation 的 input 中以 { messages, tools } 结构传入,
以便在 Langfuse UI 中查看完整的工具定义信息。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: 添加对 ACP 协议的支持 (#284)
* feat: 适配 zed acp 协议
* docs: 完善 acp 文档
* chore: 1.4.0
* conflict: 解决冲突
* feat: 添加测试覆盖率上报
* style: 改名加移动文件夹位置
* refactor: 移动测试用例及实现
* test: 修复测试用例完成
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cheng Zi Feng <1154238323@qq.com>
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
Co-authored-by: claude-code-best <272536312+claude-code-best@users.noreply.github.com>
239 lines
7.1 KiB
TypeScript
239 lines
7.1 KiB
TypeScript
import type { APIError } from '@anthropic-ai/sdk'
|
|
|
|
// SSL/TLS error codes from OpenSSL (used by both Node.js and Bun)
|
|
// See: https://www.openssl.org/docs/man3.1/man3/X509_STORE_CTX_get_error.html
|
|
const SSL_ERROR_CODES = new Set([
|
|
// Certificate verification errors
|
|
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
'UNABLE_TO_GET_ISSUER_CERT',
|
|
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
|
|
'CERT_SIGNATURE_FAILURE',
|
|
'CERT_NOT_YET_VALID',
|
|
'CERT_HAS_EXPIRED',
|
|
'CERT_REVOKED',
|
|
'CERT_REJECTED',
|
|
'CERT_UNTRUSTED',
|
|
// Self-signed certificate errors
|
|
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
// Chain errors
|
|
'CERT_CHAIN_TOO_LONG',
|
|
'PATH_LENGTH_EXCEEDED',
|
|
// Hostname/altname errors
|
|
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
'HOSTNAME_MISMATCH',
|
|
// TLS handshake errors
|
|
'ERR_TLS_HANDSHAKE_TIMEOUT',
|
|
'ERR_SSL_WRONG_VERSION_NUMBER',
|
|
'ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',
|
|
])
|
|
|
|
export type ConnectionErrorDetails = {
|
|
code: string
|
|
message: string
|
|
isSSLError: boolean
|
|
}
|
|
|
|
/**
|
|
* Extracts connection error details from the error cause chain.
|
|
* The Anthropic SDK wraps underlying errors in the `cause` property.
|
|
* This function walks the cause chain to find the root error code/message.
|
|
*/
|
|
export function extractConnectionErrorDetails(
|
|
error: unknown,
|
|
): ConnectionErrorDetails | null {
|
|
if (!error || typeof error !== 'object') {
|
|
return null
|
|
}
|
|
|
|
// Walk the cause chain to find the root error with a code
|
|
let current: unknown = error
|
|
const maxDepth = 5 // Prevent infinite loops
|
|
let depth = 0
|
|
|
|
while (current && depth < maxDepth) {
|
|
if (
|
|
current instanceof Error &&
|
|
'code' in current &&
|
|
typeof current.code === 'string'
|
|
) {
|
|
const code = current.code
|
|
const isSSLError = SSL_ERROR_CODES.has(code)
|
|
return {
|
|
code,
|
|
message: current.message,
|
|
isSSLError,
|
|
}
|
|
}
|
|
|
|
// Move to the next cause in the chain
|
|
if (
|
|
current instanceof Error &&
|
|
'cause' in current &&
|
|
current.cause !== current
|
|
) {
|
|
current = current.cause
|
|
depth++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Returns an actionable hint for SSL/TLS errors, intended for contexts outside
|
|
* the main API client (OAuth token exchange, preflight connectivity checks)
|
|
* where `formatAPIError` doesn't apply.
|
|
*/
|
|
export function getSSLErrorHint(error: unknown): string | null {
|
|
const details = extractConnectionErrorDetails(error)
|
|
if (!details?.isSSLError) {
|
|
return null
|
|
}
|
|
return `SSL certificate error (${details.code}). If you are behind a corporate proxy or TLS-intercepting firewall, set NODE_EXTRA_CA_CERTS to your CA bundle path, or ask IT to allowlist *.anthropic.com. Run /doctor for details.`
|
|
}
|
|
|
|
/**
|
|
* Strips HTML content (e.g., CloudFlare error pages) from a message string,
|
|
* returning a user-friendly title or empty string if HTML is detected.
|
|
* Returns the original message unchanged if no HTML is found.
|
|
*/
|
|
function sanitizeMessageHTML(message: string): string {
|
|
if (message.includes('<!DOCTYPE html') || message.includes('<html')) {
|
|
const titleMatch = message.match(/<title>([^<]+)<\/title>/)
|
|
if (titleMatch && titleMatch[1]) {
|
|
return titleMatch[1].trim()
|
|
}
|
|
return ''
|
|
}
|
|
return message
|
|
}
|
|
|
|
/**
|
|
* Detects if an error message contains HTML content (e.g., CloudFlare error pages)
|
|
* and returns a user-friendly message instead
|
|
*/
|
|
export function sanitizeAPIError(apiError: APIError): string {
|
|
const message = apiError.message
|
|
if (!message) {
|
|
return ''
|
|
}
|
|
return sanitizeMessageHTML(message)
|
|
}
|
|
|
|
/**
|
|
* Shapes of deserialized API errors from session JSONL.
|
|
*/
|
|
type NestedAPIError = {
|
|
error?: {
|
|
message?: string
|
|
error?: { message?: string }
|
|
}
|
|
}
|
|
|
|
function hasNestedError(value: unknown): value is NestedAPIError {
|
|
return (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
'error' in value &&
|
|
typeof value.error === 'object' &&
|
|
value.error !== null
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Extract a human-readable message from a deserialized API error that lacks
|
|
* a top-level `.message`.
|
|
*/
|
|
function extractNestedErrorMessage(error: APIError): string | null {
|
|
if (!hasNestedError(error)) {
|
|
return null
|
|
}
|
|
|
|
const narrowed: NestedAPIError = error
|
|
const nested = narrowed.error
|
|
|
|
// Standard Anthropic API shape: { error: { error: { message } } }
|
|
const deepMsg = nested?.error?.message
|
|
if (typeof deepMsg === 'string' && deepMsg.length > 0) {
|
|
const sanitized = sanitizeMessageHTML(deepMsg)
|
|
if (sanitized.length > 0) {
|
|
return sanitized
|
|
}
|
|
}
|
|
|
|
// Bedrock shape: { error: { message } }
|
|
const msg = nested?.message
|
|
if (typeof msg === 'string' && msg.length > 0) {
|
|
const sanitized = sanitizeMessageHTML(msg)
|
|
if (sanitized.length > 0) {
|
|
return sanitized
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function formatAPIError(error: APIError): string {
|
|
// Extract connection error details from the cause chain
|
|
const connectionDetails = extractConnectionErrorDetails(error)
|
|
|
|
if (connectionDetails) {
|
|
const { code, isSSLError } = connectionDetails
|
|
|
|
// Handle timeout errors
|
|
if (code === 'ETIMEDOUT') {
|
|
return 'Request timed out. Check your internet connection and proxy settings'
|
|
}
|
|
|
|
// Handle SSL/TLS errors with specific messages
|
|
if (isSSLError) {
|
|
switch (code) {
|
|
case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
|
|
case 'UNABLE_TO_GET_ISSUER_CERT':
|
|
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':
|
|
return 'Unable to connect to API: SSL certificate verification failed. Check your proxy or corporate SSL certificates'
|
|
case 'CERT_HAS_EXPIRED':
|
|
return 'Unable to connect to API: SSL certificate has expired'
|
|
case 'CERT_REVOKED':
|
|
return 'Unable to connect to API: SSL certificate has been revoked'
|
|
case 'DEPTH_ZERO_SELF_SIGNED_CERT':
|
|
case 'SELF_SIGNED_CERT_IN_CHAIN':
|
|
return 'Unable to connect to API: Self-signed certificate detected. Check your proxy or corporate SSL certificates'
|
|
case 'ERR_TLS_CERT_ALTNAME_INVALID':
|
|
case 'HOSTNAME_MISMATCH':
|
|
return 'Unable to connect to API: SSL certificate hostname mismatch'
|
|
case 'CERT_NOT_YET_VALID':
|
|
return 'Unable to connect to API: SSL certificate is not yet valid'
|
|
default:
|
|
return `Unable to connect to API: SSL error (${code})`
|
|
}
|
|
}
|
|
}
|
|
|
|
if (error.message === 'Connection error.') {
|
|
// If we have a code but it's not SSL, include it for debugging
|
|
if (connectionDetails?.code) {
|
|
return `Unable to connect to API (${connectionDetails.code})`
|
|
}
|
|
return 'Unable to connect to API. Check your internet connection'
|
|
}
|
|
|
|
// Guard: when deserialized from JSONL (e.g. --resume), the error object may
|
|
// be a plain object without a `.message` property.
|
|
if (!error.message) {
|
|
return (
|
|
extractNestedErrorMessage(error) ??
|
|
`API error (status ${error.status ?? 'unknown'})`
|
|
)
|
|
}
|
|
|
|
const sanitizedMessage = sanitizeAPIError(error)
|
|
// Use sanitized message if it's different from the original (i.e., HTML was sanitized)
|
|
return sanitizedMessage !== error.message && sanitizedMessage.length > 0
|
|
? sanitizedMessage
|
|
: error.message
|
|
}
|