mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-24 09:05:50 +00:00
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>
This commit is contained in:
238
packages/@anthropic-ai/model-provider/src/errorUtils.ts
Normal file
238
packages/@anthropic-ai/model-provider/src/errorUtils.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ import type {
|
|||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.js'
|
||||||
import type { BetaMessageDeltaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type { BetaMessageDeltaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import { accumulateUsage, updateUsage } from 'src/services/api/claude.js'
|
import { accumulateUsage, updateUsage } from 'src/services/api/claude.js'
|
||||||
import type { NonNullableUsage } from 'src/services/api/logging.js'
|
import type { NonNullableUsage } from '@anthropic-ai/model-provider'
|
||||||
import { EMPTY_USAGE } from 'src/services/api/logging.js'
|
import { EMPTY_USAGE } from '@anthropic-ai/model-provider'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
import type { Command } from './commands.js'
|
import type { Command } from './commands.js'
|
||||||
import { getSlashCommandToolSkills } from './commands.js'
|
import { getSlashCommandToolSkills } from './commands.js'
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
} from '../entrypoints/sdk/controlTypes.js'
|
} from '../entrypoints/sdk/controlTypes.js'
|
||||||
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
|
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
|
||||||
import { logEvent } from '../services/analytics/index.js'
|
import { logEvent } from '../services/analytics/index.js'
|
||||||
import { EMPTY_USAGE } from '../services/api/emptyUsage.js'
|
import { EMPTY_USAGE } from '@anthropic-ai/model-provider'
|
||||||
import type { Message } from '../types/message.js'
|
import type { Message } from '../types/message.js'
|
||||||
import { normalizeControlMessageKeys } from '../utils/controlMessageCompat.js'
|
import { normalizeControlMessageKeys } from '../utils/controlMessageCompat.js'
|
||||||
import { logForDebugging } from '../utils/debug.js'
|
import { logForDebugging } from '../utils/debug.js'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
logEvent,
|
logEvent,
|
||||||
} from '../../services/analytics/index.js'
|
} from '../../services/analytics/index.js'
|
||||||
import { getSSLErrorHint } from '../../services/api/errorUtils.js'
|
import { getSSLErrorHint } from '@anthropic-ai/model-provider'
|
||||||
import { fetchAndStoreClaudeCodeFirstTokenDate } from '../../services/api/firstTokenDate.js'
|
import { fetchAndStoreClaudeCodeFirstTokenDate } from '../../services/api/firstTokenDate.js'
|
||||||
import {
|
import {
|
||||||
createAndStoreApiKey,
|
createAndStoreApiKey,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ import {
|
|||||||
registerProcessOutputErrorHandlers,
|
registerProcessOutputErrorHandlers,
|
||||||
} from 'src/utils/process.js'
|
} from 'src/utils/process.js'
|
||||||
import type { Stream } from 'src/utils/stream.js'
|
import type { Stream } from 'src/utils/stream.js'
|
||||||
import { EMPTY_USAGE } from 'src/services/api/logging.js'
|
import { EMPTY_USAGE } from '@anthropic-ai/model-provider'
|
||||||
import {
|
import {
|
||||||
loadConversationForResume,
|
loadConversationForResume,
|
||||||
type TurnInterruptionState,
|
type TurnInterruptionState,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { installOAuthTokens } from '../cli/handlers/auth.js'
|
|||||||
import { useTerminalSize } from '../hooks/useTerminalSize.js'
|
import { useTerminalSize } from '../hooks/useTerminalSize.js'
|
||||||
import { setClipboard, useTerminalNotification, Box, Link, Text, KeyboardShortcutHint } from '@anthropic/ink'
|
import { setClipboard, useTerminalNotification, Box, Link, Text, KeyboardShortcutHint } from '@anthropic/ink'
|
||||||
import { useKeybinding } from '../keybindings/useKeybinding.js'
|
import { useKeybinding } from '../keybindings/useKeybinding.js'
|
||||||
import { getSSLErrorHint } from '../services/api/errorUtils.js'
|
import { getSSLErrorHint } from '@anthropic-ai/model-provider'
|
||||||
import { sendNotification } from '../services/notifier.js'
|
import { sendNotification } from '../services/notifier.js'
|
||||||
import { OAuthService } from '../services/oauth/index.js'
|
import { OAuthService } from '../services/oauth/index.js'
|
||||||
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'
|
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '@anthropic/ink'
|
||||||
import { formatAPIError } from 'src/services/api/errorUtils.js'
|
import { formatAPIError } from '@anthropic-ai/model-provider'
|
||||||
import type { SystemAPIErrorMessage } from 'src/types/message.js'
|
import type { SystemAPIErrorMessage } from 'src/types/message.js'
|
||||||
import { useInterval } from 'usehooks-ts'
|
import { useInterval } from 'usehooks-ts'
|
||||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||||
|
|||||||
@@ -1,24 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Stub: SDK Utility Types.
|
* Stub: SDK Utility Types.
|
||||||
|
* Re-exported from @anthropic-ai/model-provider.
|
||||||
*/
|
*/
|
||||||
export type NonNullableUsage = {
|
export type { NonNullableUsage } from '@anthropic-ai/model-provider'
|
||||||
inputTokens?: number
|
|
||||||
outputTokens?: number
|
|
||||||
cacheReadInputTokens?: number
|
|
||||||
cacheCreationInputTokens?: number
|
|
||||||
input_tokens: number
|
|
||||||
cache_creation_input_tokens: number
|
|
||||||
cache_read_input_tokens: number
|
|
||||||
output_tokens: number
|
|
||||||
server_tool_use: { web_search_requests: number; web_fetch_requests: number }
|
|
||||||
service_tier: string
|
|
||||||
cache_creation: {
|
|
||||||
ephemeral_1h_input_tokens: number
|
|
||||||
ephemeral_5m_input_tokens: number
|
|
||||||
}
|
|
||||||
inference_geo: string
|
|
||||||
iterations: unknown[]
|
|
||||||
speed: string
|
|
||||||
cache_deleted_input_tokens?: number
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,4 @@
|
|||||||
import type { NonNullableUsage } from '../../entrypoints/sdk/sdkUtilityTypes.js'
|
// Re-export EMPTY_USAGE from @anthropic-ai/model-provider
|
||||||
|
// Kept here for backward compatibility — consumers import from this path.
|
||||||
/**
|
export { EMPTY_USAGE } from '@anthropic-ai/model-provider'
|
||||||
* Zero-initialized usage object. Extracted from logging.ts so that
|
export type { NonNullableUsage } from '@anthropic-ai/model-provider'
|
||||||
* bridge/replBridge.ts can import it without transitively pulling in
|
|
||||||
* api/errors.ts → utils/messages.ts → BashTool.tsx → the world.
|
|
||||||
*/
|
|
||||||
export const EMPTY_USAGE: Readonly<NonNullableUsage> = {
|
|
||||||
input_tokens: 0,
|
|
||||||
cache_creation_input_tokens: 0,
|
|
||||||
cache_read_input_tokens: 0,
|
|
||||||
output_tokens: 0,
|
|
||||||
server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 },
|
|
||||||
service_tier: 'standard',
|
|
||||||
cache_creation: {
|
|
||||||
ephemeral_1h_input_tokens: 0,
|
|
||||||
ephemeral_5m_input_tokens: 0,
|
|
||||||
},
|
|
||||||
inference_geo: '',
|
|
||||||
iterations: [],
|
|
||||||
speed: 'standard',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,260 +1,8 @@
|
|||||||
import type { APIError } from '@anthropic-ai/sdk'
|
// Re-export from @anthropic-ai/model-provider
|
||||||
|
export {
|
||||||
// SSL/TLS error codes from OpenSSL (used by both Node.js and Bun)
|
formatAPIError,
|
||||||
// See: https://www.openssl.org/docs/man3.1/man3/X509_STORE_CTX_get_error.html
|
extractConnectionErrorDetails,
|
||||||
const SSL_ERROR_CODES = new Set([
|
sanitizeAPIError,
|
||||||
// Certificate verification errors
|
getSSLErrorHint,
|
||||||
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
type ConnectionErrorDetails,
|
||||||
'UNABLE_TO_GET_ISSUER_CERT',
|
} from '@anthropic-ai/model-provider'
|
||||||
'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.
|
|
||||||
*
|
|
||||||
* Motivation: enterprise users behind TLS-intercepting proxies (Zscaler et al.)
|
|
||||||
* see OAuth complete in-browser but the CLI's token exchange silently fails
|
|
||||||
* with a raw SSL code. Surfacing the likely fix saves a support round-trip.
|
|
||||||
*/
|
|
||||||
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) {
|
|
||||||
// Sometimes message is undefined
|
|
||||||
// TODO: figure out why
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return sanitizeMessageHTML(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shapes of deserialized API errors from session JSONL.
|
|
||||||
*
|
|
||||||
* After JSON round-tripping, the SDK's APIError loses its `.message` property.
|
|
||||||
* The actual message lives at different nesting levels depending on the provider:
|
|
||||||
*
|
|
||||||
* - Bedrock/proxy: `{ error: { message: "..." } }`
|
|
||||||
* - Standard Anthropic API: `{ error: { error: { message: "..." } } }`
|
|
||||||
* (the outer `.error` is the response body, the inner `.error` is the API error)
|
|
||||||
*
|
|
||||||
* See also: `getErrorMessage` in `logging.ts` which handles the same shapes.
|
|
||||||
*/
|
|
||||||
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`.
|
|
||||||
*
|
|
||||||
* Checks two nesting levels (deeper first for specificity):
|
|
||||||
* 1. `error.error.error.message` — standard Anthropic API shape
|
|
||||||
* 2. `error.error.message` — Bedrock shape
|
|
||||||
*/
|
|
||||||
function extractNestedErrorMessage(error: APIError): string | null {
|
|
||||||
if (!hasNestedError(error)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access `.error` via the narrowed type so TypeScript sees the nested shape
|
|
||||||
// instead of the SDK's `Object | undefined`.
|
|
||||||
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. Return a safe fallback
|
|
||||||
// instead of undefined, which would crash callers that access `.length`.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,141 +1,74 @@
|
|||||||
// Auto-generated stub — replace with real implementation
|
// Re-export core message types from @anthropic-ai/model-provider
|
||||||
import type { UUID } from 'crypto'
|
// This file adds UI-specific types on top of the base types.
|
||||||
import type {
|
export type {
|
||||||
ContentBlockParam,
|
MessageType,
|
||||||
ContentBlock,
|
ContentItem,
|
||||||
} from '@anthropic-ai/sdk/resources/index.mjs'
|
MessageContent,
|
||||||
import type { BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
TypedMessageContent,
|
||||||
|
Message,
|
||||||
|
AssistantMessage,
|
||||||
|
AttachmentMessage,
|
||||||
|
ProgressMessage,
|
||||||
|
SystemLocalCommandMessage,
|
||||||
|
SystemMessage,
|
||||||
|
UserMessage,
|
||||||
|
NormalizedUserMessage,
|
||||||
|
RequestStartEvent,
|
||||||
|
StreamEvent,
|
||||||
|
SystemCompactBoundaryMessage,
|
||||||
|
TombstoneMessage,
|
||||||
|
ToolUseSummaryMessage,
|
||||||
|
MessageOrigin,
|
||||||
|
CompactMetadata,
|
||||||
|
SystemAPIErrorMessage,
|
||||||
|
SystemFileSnapshotMessage,
|
||||||
|
NormalizedAssistantMessage,
|
||||||
|
NormalizedMessage,
|
||||||
|
PartialCompactDirection,
|
||||||
|
StopHookInfo,
|
||||||
|
SystemAgentsKilledMessage,
|
||||||
|
SystemApiMetricsMessage,
|
||||||
|
SystemAwaySummaryMessage,
|
||||||
|
SystemBridgeStatusMessage,
|
||||||
|
SystemInformationalMessage,
|
||||||
|
SystemMemorySavedMessage,
|
||||||
|
SystemMessageLevel,
|
||||||
|
SystemMicrocompactBoundaryMessage,
|
||||||
|
SystemPermissionRetryMessage,
|
||||||
|
SystemScheduledTaskFireMessage,
|
||||||
|
SystemStopHookSummaryMessage,
|
||||||
|
SystemTurnDurationMessage,
|
||||||
|
GroupedToolUseMessage,
|
||||||
|
CollapsibleMessage,
|
||||||
|
HookResultMessage,
|
||||||
|
SystemThinkingMessage,
|
||||||
|
} from '@anthropic-ai/model-provider'
|
||||||
|
|
||||||
|
// UI-specific types that depend on main-project internals
|
||||||
import type {
|
import type {
|
||||||
BranchAction,
|
BranchAction,
|
||||||
CommitKind,
|
CommitKind,
|
||||||
PrAction,
|
PrAction,
|
||||||
} from '@claude-code-best/builtin-tools/tools/shared/gitOperationTracking.js'
|
} from '@claude-code-best/builtin-tools/tools/shared/gitOperationTracking.js'
|
||||||
|
import type {
|
||||||
/**
|
AssistantMessage,
|
||||||
* Base message type with discriminant `type` field and common properties.
|
CollapsibleMessage,
|
||||||
* Individual message subtypes (UserMessage, AssistantMessage, etc.) extend
|
NormalizedAssistantMessage,
|
||||||
* this with narrower `type` literals and additional fields.
|
NormalizedUserMessage,
|
||||||
*/
|
UserMessage,
|
||||||
export type MessageType = 'user' | 'assistant' | 'system' | 'attachment' | 'progress' | 'grouped_tool_use' | 'collapsed_read_search'
|
} from '@anthropic-ai/model-provider'
|
||||||
|
import type { UUID } from 'crypto'
|
||||||
/** A single content element inside message.content arrays. */
|
import type { StopHookInfo } from '@anthropic-ai/model-provider'
|
||||||
export type ContentItem = ContentBlockParam | ContentBlock
|
|
||||||
|
|
||||||
export type MessageContent = string | ContentBlockParam[] | ContentBlock[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Typed content array — used in narrowed message subtypes so that
|
|
||||||
* `message.content[0]` resolves to `ContentItem` instead of
|
|
||||||
* `string | ContentBlockParam | ContentBlock`.
|
|
||||||
*/
|
|
||||||
export type TypedMessageContent = ContentItem[]
|
|
||||||
|
|
||||||
export type Message = {
|
|
||||||
type: MessageType
|
|
||||||
uuid: UUID
|
|
||||||
isMeta?: boolean
|
|
||||||
isCompactSummary?: boolean
|
|
||||||
toolUseResult?: unknown
|
|
||||||
isVisibleInTranscriptOnly?: boolean
|
|
||||||
attachment?: { type: string; toolUseID?: string; [key: string]: unknown; addedNames: string[]; addedLines: string[]; removedNames: string[] }
|
|
||||||
message?: {
|
|
||||||
role?: string
|
|
||||||
id?: string
|
|
||||||
content?: MessageContent
|
|
||||||
usage?: BetaUsage | Record<string, unknown>
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AssistantMessage = Message & {
|
|
||||||
type: 'assistant'
|
|
||||||
message: NonNullable<Message['message']>
|
|
||||||
}
|
|
||||||
export type AttachmentMessage<T = { type: string; [key: string]: unknown }> = Message & { type: 'attachment'; attachment: T }
|
|
||||||
export type ProgressMessage<T = unknown> = Message & { type: 'progress'; data: T }
|
|
||||||
export type SystemLocalCommandMessage = Message & { type: 'system' }
|
|
||||||
export type SystemMessage = Message & { type: 'system' }
|
|
||||||
export type UserMessage = Message & {
|
|
||||||
type: 'user'
|
|
||||||
message: NonNullable<Message['message']>
|
|
||||||
imagePasteIds?: number[]
|
|
||||||
}
|
|
||||||
export type NormalizedUserMessage = UserMessage
|
|
||||||
export type RequestStartEvent = { type: string; [key: string]: unknown }
|
|
||||||
export type StreamEvent = { type: string; [key: string]: unknown }
|
|
||||||
export type SystemCompactBoundaryMessage = Message & {
|
|
||||||
type: 'system'
|
|
||||||
compactMetadata: {
|
|
||||||
preservedSegment?: {
|
|
||||||
headUuid: UUID
|
|
||||||
tailUuid: UUID
|
|
||||||
anchorUuid: UUID
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export type TombstoneMessage = Message
|
|
||||||
export type ToolUseSummaryMessage = Message
|
|
||||||
export type MessageOrigin = string
|
|
||||||
export type CompactMetadata = Record<string, unknown>
|
|
||||||
export type SystemAPIErrorMessage = Message & { type: 'system' }
|
|
||||||
export type SystemFileSnapshotMessage = Message & { type: 'system' }
|
|
||||||
export type NormalizedAssistantMessage<T = unknown> = AssistantMessage
|
|
||||||
export type NormalizedMessage = Message
|
|
||||||
export type PartialCompactDirection = string
|
|
||||||
|
|
||||||
export type StopHookInfo = {
|
|
||||||
command?: string
|
|
||||||
durationMs?: number
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SystemAgentsKilledMessage = Message & { type: 'system' }
|
|
||||||
export type SystemApiMetricsMessage = Message & { type: 'system' }
|
|
||||||
export type SystemAwaySummaryMessage = Message & { type: 'system' }
|
|
||||||
export type SystemBridgeStatusMessage = Message & { type: 'system' }
|
|
||||||
export type SystemInformationalMessage = Message & { type: 'system' }
|
|
||||||
export type SystemMemorySavedMessage = Message & { type: 'system' }
|
|
||||||
export type SystemMessageLevel = string
|
|
||||||
export type SystemMicrocompactBoundaryMessage = Message & { type: 'system' }
|
|
||||||
export type SystemPermissionRetryMessage = Message & { type: 'system' }
|
|
||||||
export type SystemScheduledTaskFireMessage = Message & { type: 'system' }
|
|
||||||
|
|
||||||
export type SystemStopHookSummaryMessage = Message & {
|
|
||||||
type: 'system'
|
|
||||||
subtype: string
|
|
||||||
hookLabel: string
|
|
||||||
hookCount: number
|
|
||||||
totalDurationMs?: number
|
|
||||||
hookInfos: StopHookInfo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SystemTurnDurationMessage = Message & { type: 'system' }
|
|
||||||
|
|
||||||
export type GroupedToolUseMessage = Message & {
|
|
||||||
type: 'grouped_tool_use'
|
|
||||||
toolName: string
|
|
||||||
messages: NormalizedAssistantMessage[]
|
|
||||||
results: NormalizedUserMessage[]
|
|
||||||
displayMessage: NormalizedAssistantMessage | NormalizedUserMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RenderableMessage =
|
export type RenderableMessage =
|
||||||
| AssistantMessage
|
| AssistantMessage
|
||||||
| UserMessage
|
| UserMessage
|
||||||
| (Message & { type: 'system' })
|
| (import('@anthropic-ai/model-provider').Message & { type: 'system' })
|
||||||
| (Message & { type: 'attachment'; attachment: { type: string; memories?: { path: string; content: string; mtimeMs: number }[]; [key: string]: unknown } })
|
| (import('@anthropic-ai/model-provider').Message & { type: 'attachment'; attachment: { type: string; memories?: { path: string; content: string; mtimeMs: number }[]; [key: string]: unknown } })
|
||||||
| (Message & { type: 'progress' })
|
| (import('@anthropic-ai/model-provider').Message & { type: 'progress' })
|
||||||
| GroupedToolUseMessage
|
| import('@anthropic-ai/model-provider').GroupedToolUseMessage
|
||||||
| CollapsedReadSearchGroup
|
| CollapsedReadSearchGroup
|
||||||
|
|
||||||
export type CollapsibleMessage =
|
|
||||||
| AssistantMessage
|
|
||||||
| UserMessage
|
|
||||||
| GroupedToolUseMessage
|
|
||||||
|
|
||||||
export type CollapsedReadSearchGroup = {
|
export type CollapsedReadSearchGroup = {
|
||||||
type: 'collapsed_read_search'
|
type: 'collapsed_read_search'
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
@@ -169,6 +102,3 @@ export type CollapsedReadSearchGroup = {
|
|||||||
teamMemoryWriteCount?: number
|
teamMemoryWriteCount?: number
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HookResultMessage = Message
|
|
||||||
export type SystemThinkingMessage = Message & { type: 'system' }
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
logEvent,
|
logEvent,
|
||||||
} from '../services/analytics/index.js'
|
} from '../services/analytics/index.js'
|
||||||
import { accumulateUsage, updateUsage } from '../services/api/claude.js'
|
import { accumulateUsage, updateUsage } from '../services/api/claude.js'
|
||||||
import { EMPTY_USAGE, type NonNullableUsage } from '../services/api/logging.js'
|
import { EMPTY_USAGE, type NonNullableUsage } from '@anthropic-ai/model-provider'
|
||||||
import type { ToolUseContext } from '../Tool.js'
|
import type { ToolUseContext } from '../Tool.js'
|
||||||
import type { AgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
|
import type { AgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
|
||||||
import type { AgentId } from '../types/ids.js'
|
import type { AgentId } from '../types/ids.js'
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* while keeping the side question response separate from main conversation.
|
* while keeping the side question response separate from main conversation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { formatAPIError } from '../services/api/errorUtils.js'
|
import { formatAPIError } from '@anthropic-ai/model-provider'
|
||||||
import type { NonNullableUsage } from '../services/api/logging.js'
|
import type { NonNullableUsage } from '@anthropic-ai/model-provider'
|
||||||
import type { Message, SystemAPIErrorMessage } from '../types/message.js'
|
import type { Message, SystemAPIErrorMessage } from '../types/message.js'
|
||||||
import { type CacheSafeParams, runForkedAgent } from './forkedAgent.js'
|
import { type CacheSafeParams, runForkedAgent } from './forkedAgent.js'
|
||||||
import { createUserMessage, extractTextContent } from './messages.js'
|
import { createUserMessage, extractTextContent } from './messages.js'
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
/**
|
// Re-export SystemPrompt from @anthropic-ai/model-provider
|
||||||
* Branded type for system prompt arrays.
|
// Kept here for backward compatibility.
|
||||||
*
|
export type { SystemPrompt } from '@anthropic-ai/model-provider'
|
||||||
* This module is intentionally dependency-free so it can be imported
|
export { asSystemPrompt } from '@anthropic-ai/model-provider'
|
||||||
* from anywhere without risking circular initialization issues.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type SystemPrompt = readonly string[] & {
|
|
||||||
readonly __brand: 'SystemPrompt'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function asSystemPrompt(value: readonly string[]): SystemPrompt {
|
|
||||||
return value as SystemPrompt
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user