Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
42 kB · 1207 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208import { APIConnectionError, APIConnectionTimeoutError, APIError,} from '@anthropic-ai/sdk'import type { BetaMessage, BetaStopReason,} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'import { AFK_MODE_BETA_HEADER } from 'src/constants/betas.js'import type { SDKAssistantMessageError } from 'src/entrypoints/agentSdkTypes.js'import type { AssistantMessage, Message, UserMessage,} from 'src/types/message.js'import { getAnthropicApiKeyWithSource, getClaudeAIOAuthTokens, getOauthAccountInfo, isClaudeAISubscriber,} from 'src/utils/auth.js'import { createAssistantAPIErrorMessage, NO_RESPONSE_REQUESTED,} from 'src/utils/messages.js'import { getDefaultMainLoopModelSetting, isNonCustomOpusModel,} from 'src/utils/model/model.js'import { getModelStrings } from 'src/utils/model/modelStrings.js'import { getAPIProvider } from 'src/utils/model/providers.js'import { getIsNonInteractiveSession } from '../../bootstrap/state.js'import { API_PDF_MAX_PAGES, PDF_TARGET_RAW_SIZE,} from '../../constants/apiLimits.js'import { isEnvTruthy } from '../../utils/envUtils.js'import { formatFileSize } from '../../utils/format.js'import { ImageResizeError } from '../../utils/imageResizer.js'import { ImageSizeError } from '../../utils/imageValidation.js'import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent,} from '../analytics/index.js'import { type ClaudeAILimits, getRateLimitErrorMessage, type OverageDisabledReason,} from '../claudeAiLimits.js'import { shouldProcessRateLimits } from '../rateLimitMocking.js' // Used for /mock-limits commandimport { extractConnectionErrorDetails, formatAPIError } from './errorUtils.js'
export const API_ERROR_MESSAGE_PREFIX = 'API Error'
export function startsWithApiErrorPrefix(text: string): boolean { return ( text.startsWith(API_ERROR_MESSAGE_PREFIX) || text.startsWith(`Please run /login · ${API_ERROR_MESSAGE_PREFIX}`) )}export const PROMPT_TOO_LONG_ERROR_MESSAGE = 'Prompt is too long'
export function isPromptTooLongMessage(msg: AssistantMessage): boolean { if (!msg.isApiErrorMessage) { return false } const content = msg.message.content if (!Array.isArray(content)) { return false } return content.some( block => block.type === 'text' && block.text.startsWith(PROMPT_TOO_LONG_ERROR_MESSAGE), )}
/** * Parse actual/limit token counts from a raw prompt-too-long API error * message like "prompt is too long: 137500 tokens > 135000 maximum". * The raw string may be wrapped in SDK prefixes or JSON envelopes, or * have different casing (Vertex), so this is intentionally lenient. */export function parsePromptTooLongTokenCounts(rawMessage: string): { actualTokens: number | undefined limitTokens: number | undefined} { const match = rawMessage.match( /prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)/i, ) return { actualTokens: match ? parseInt(match[1]!, 10) : undefined, limitTokens: match ? parseInt(match[2]!, 10) : undefined, }}
/** * Returns how many tokens over the limit a prompt-too-long error reports, * or undefined if the message isn't PTL or its errorDetails are unparseable. * Reactive compact uses this gap to jump past multiple groups in one retry * instead of peeling one-at-a-time. */export function getPromptTooLongTokenGap( msg: AssistantMessage,): number | undefined { if (!isPromptTooLongMessage(msg) || !msg.errorDetails) { return undefined } const { actualTokens, limitTokens } = parsePromptTooLongTokenCounts( msg.errorDetails, ) if (actualTokens === undefined || limitTokens === undefined) { return undefined } const gap = actualTokens - limitTokens return gap > 0 ? gap : undefined}
/** * Is this raw API error text a media-size rejection that stripImagesFromMessages * can fix? Reactive compact's summarize retry uses this to decide whether to * strip and retry (media error) or bail (anything else). * * Patterns MUST stay in sync with the getAssistantMessageFromError branches * that populate errorDetails (~L523 PDF, ~L560 image, ~L573 many-image) and * the classifyAPIError branches (~L929-946). The closed loop: errorDetails is * only set after those branches already matched these same substrings, so * isMediaSizeError(errorDetails) is tautologically true for that path. API * wording drift causes graceful degradation (errorDetails stays undefined, * caller short-circuits), not a false negative. */export function isMediaSizeError(raw: string): boolean { return ( (raw.includes('image exceeds') && raw.includes('maximum')) || (raw.includes('image dimensions exceed') && raw.includes('many-image')) || /maximum of \d+ PDF pages/.test(raw) )}
/** * Message-level predicate: is this assistant message a media-size rejection? * Parallel to isPromptTooLongMessage. Checks errorDetails (the raw API error * string populated by the getAssistantMessageFromError branches at ~L523/560/573) * rather than content text, since media errors have per-variant content strings. */export function isMediaSizeErrorMessage(msg: AssistantMessage): boolean { return ( msg.isApiErrorMessage === true && msg.errorDetails !== undefined && isMediaSizeError(msg.errorDetails) )}export const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE = 'Credit balance is too low'export const INVALID_API_KEY_ERROR_MESSAGE = 'Not logged in · Please run /login'export const INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL = 'Invalid API key · Fix external API key'export const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH = 'Your ANTHROPIC_API_KEY belongs to a disabled organization · Unset the environment variable to use your subscription instead'export const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY = 'Your ANTHROPIC_API_KEY belongs to a disabled organization · Update or unset the environment variable'export const TOKEN_REVOKED_ERROR_MESSAGE = 'OAuth token revoked · Please run /login'export const CCR_AUTH_ERROR_MESSAGE = 'Authentication error · This may be a temporary network issue, please try again'export const REPEATED_529_ERROR_MESSAGE = 'Repeated 529 Overloaded errors'export const CUSTOM_OFF_SWITCH_MESSAGE = 'Opus is experiencing high load, please use /model to switch to Sonnet'export const API_TIMEOUT_ERROR_MESSAGE = 'Request timed out'export function getPdfTooLargeErrorMessage(): string { const limits = `max ${API_PDF_MAX_PAGES} pages, ${formatFileSize(PDF_TARGET_RAW_SIZE)}` return getIsNonInteractiveSession() ? `PDF too large (${limits}). Try reading the file a different way (e.g., extract text with pdftotext).` : `PDF too large (${limits}). Double press esc to go back and try again, or use pdftotext to convert to text first.`}export function getPdfPasswordProtectedErrorMessage(): string { return getIsNonInteractiveSession() ? 'PDF is password protected. Try using a CLI tool to extract or convert the PDF.' : 'PDF is password protected. Please double press esc to edit your message and try again.'}export function getPdfInvalidErrorMessage(): string { return getIsNonInteractiveSession() ? 'The PDF file was not valid. Try converting it to text first (e.g., pdftotext).' : 'The PDF file was not valid. Double press esc to go back and try again with a different file.'}export function getImageTooLargeErrorMessage(): string { return getIsNonInteractiveSession() ? 'Image was too large. Try resizing the image or using a different approach.' : 'Image was too large. Double press esc to go back and try again with a smaller image.'}export function getRequestTooLargeErrorMessage(): string { const limits = `max ${formatFileSize(PDF_TARGET_RAW_SIZE)}` return getIsNonInteractiveSession() ? `Request too large (${limits}). Try with a smaller file.` : `Request too large (${limits}). Double press esc to go back and try with a smaller file.`}export const OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE = 'Your account does not have access to Claude Code. Please run /login.'
export function getTokenRevokedErrorMessage(): string { return getIsNonInteractiveSession() ? 'Your account does not have access to Claude. Please login again or contact your administrator.' : TOKEN_REVOKED_ERROR_MESSAGE}
export function getOauthOrgNotAllowedErrorMessage(): string { return getIsNonInteractiveSession() ? 'Your organization does not have access to Claude. Please login again or contact your administrator.' : OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE}
/** * Check if we're in CCR (Claude Code Remote) mode. * In CCR mode, auth is handled via JWTs provided by the infrastructure, * not via /login. Transient auth errors should suggest retrying, not logging in. */function isCCRMode(): boolean { return isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)}
// Temp helper to log tool_use/tool_result mismatch errorsfunction logToolUseToolResultMismatch( toolUseId: string, messages: Message[], messagesForAPI: (UserMessage | AssistantMessage)[],): void { try { // Find tool_use in normalized messages let normalizedIndex = -1 for (let i = 0; i < messagesForAPI.length; i++) { const msg = messagesForAPI[i] if (!msg) continue const content = msg.message.content if (Array.isArray(content)) { for (const block of content) { if ( block.type === 'tool_use' && 'id' in block && block.id === toolUseId ) { normalizedIndex = i break } } } if (normalizedIndex !== -1) break }
// Find tool_use in original messages let originalIndex = -1 for (let i = 0; i < messages.length; i++) { const msg = messages[i] if (!msg) continue if (msg.type === 'assistant' && 'message' in msg) { const content = msg.message.content if (Array.isArray(content)) { for (const block of content) { if ( block.type === 'tool_use' && 'id' in block && block.id === toolUseId ) { originalIndex = i break } } } } if (originalIndex !== -1) break }
// Build normalized sequence const normalizedSeq: string[] = [] for (let i = normalizedIndex + 1; i < messagesForAPI.length; i++) { const msg = messagesForAPI[i] if (!msg) continue const content = msg.message.content if (Array.isArray(content)) { for (const block of content) { const role = msg.message.role if (block.type === 'tool_use' && 'id' in block) { normalizedSeq.push(`${role}:tool_use:${block.id}`) } else if (block.type === 'tool_result' && 'tool_use_id' in block) { normalizedSeq.push(`${role}:tool_result:${block.tool_use_id}`) } else if (block.type === 'text') { normalizedSeq.push(`${role}:text`) } else if (block.type === 'thinking') { normalizedSeq.push(`${role}:thinking`) } else if (block.type === 'image') { normalizedSeq.push(`${role}:image`) } else { normalizedSeq.push(`${role}:${block.type}`) } } } else if (typeof content === 'string') { normalizedSeq.push(`${msg.message.role}:string_content`) } }
// Build pre-normalized sequence const preNormalizedSeq: string[] = [] for (let i = originalIndex + 1; i < messages.length; i++) { const msg = messages[i] if (!msg) continue
switch (msg.type) { case 'user': case 'assistant': { if ('message' in msg) { const content = msg.message.content if (Array.isArray(content)) { for (const block of content) { const role = msg.message.role if (block.type === 'tool_use' && 'id' in block) { preNormalizedSeq.push(`${role}:tool_use:${block.id}`) } else if ( block.type === 'tool_result' && 'tool_use_id' in block ) { preNormalizedSeq.push( `${role}:tool_result:${block.tool_use_id}`, ) } else if (block.type === 'text') { preNormalizedSeq.push(`${role}:text`) } else if (block.type === 'thinking') { preNormalizedSeq.push(`${role}:thinking`) } else if (block.type === 'image') { preNormalizedSeq.push(`${role}:image`) } else { preNormalizedSeq.push(`${role}:${block.type}`) } } } else if (typeof content === 'string') { preNormalizedSeq.push(`${msg.message.role}:string_content`) } } break } case 'attachment': if ('attachment' in msg) { preNormalizedSeq.push(`attachment:${msg.attachment.type}`) } break case 'system': if ('subtype' in msg) { preNormalizedSeq.push(`system:${msg.subtype}`) } break case 'progress': if ( 'progress' in msg && msg.progress && typeof msg.progress === 'object' && 'type' in msg.progress ) { preNormalizedSeq.push(`progress:${msg.progress.type ?? 'unknown'}`) } else { preNormalizedSeq.push('progress:unknown') } break } }
// Log to Statsig logEvent('tengu_tool_use_tool_result_mismatch_error', { toolUseId: toolUseId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, normalizedSequence: normalizedSeq.join( ', ', ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, preNormalizedSequence: preNormalizedSeq.join( ', ', ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, normalizedMessageCount: messagesForAPI.length, originalMessageCount: messages.length, normalizedToolUseIndex: normalizedIndex, originalToolUseIndex: originalIndex, }) } catch (_) { // Ignore errors in debug logging }}
/** * Type guard to check if a value is a valid Message response from the API */export function isValidAPIMessage(value: unknown): value is BetaMessage { return ( typeof value === 'object' && value !== null && 'content' in value && 'model' in value && 'usage' in value && Array.isArray((value as BetaMessage).content) && typeof (value as BetaMessage).model === 'string' && typeof (value as BetaMessage).usage === 'object' )}
/** Lower-level error that AWS can return. */type AmazonError = { Output?: { __type?: string } Version?: string}
/** * Given a response that doesn't look quite right, see if it contains any known error types we can extract. */export function extractUnknownErrorFormat(value: unknown): string | undefined { // Check if value is a valid object first if (!value || typeof value !== 'object') { return undefined }
// Amazon Bedrock routing errors if ((value as AmazonError).Output?.__type) { return (value as AmazonError).Output!.__type }
return undefined}
export function getAssistantMessageFromError( error: unknown, model: string, options?: { messages?: Message[] messagesForAPI?: (UserMessage | AssistantMessage)[] },): AssistantMessage { // Check for SDK timeout errors if ( error instanceof APIConnectionTimeoutError || (error instanceof APIConnectionError && error.message.toLowerCase().includes('timeout')) ) { return createAssistantAPIErrorMessage({ content: API_TIMEOUT_ERROR_MESSAGE, error: 'unknown', }) }
// Check for image size/resize errors (thrown before API call during validation) // Use getImageTooLargeErrorMessage() to show "esc esc" hint for CLI users // but a generic message for SDK users (non-interactive mode) if (error instanceof ImageSizeError || error instanceof ImageResizeError) { return createAssistantAPIErrorMessage({ content: getImageTooLargeErrorMessage(), }) }
// Check for emergency capacity off switch for Opus PAYG users if ( error instanceof Error && error.message.includes(CUSTOM_OFF_SWITCH_MESSAGE) ) { return createAssistantAPIErrorMessage({ content: CUSTOM_OFF_SWITCH_MESSAGE, error: 'rate_limit', }) }
if ( error instanceof APIError && error.status === 429 && shouldProcessRateLimits(isClaudeAISubscriber()) ) { // Check if this is the new API with multiple rate limit headers const rateLimitType = error.headers?.get?.( 'anthropic-ratelimit-unified-representative-claim', ) as 'five_hour' | 'seven_day' | 'seven_day_opus' | null
const overageStatus = error.headers?.get?.( 'anthropic-ratelimit-unified-overage-status', ) as 'allowed' | 'allowed_warning' | 'rejected' | null
// If we have the new headers, use the new message generation if (rateLimitType || overageStatus) { // Build limits object from error headers to determine the appropriate message const limits: ClaudeAILimits = { status: 'rejected', unifiedRateLimitFallbackAvailable: false, isUsingOverage: false, }
// Extract rate limit information from headers const resetHeader = error.headers?.get?.( 'anthropic-ratelimit-unified-reset', ) if (resetHeader) { limits.resetsAt = Number(resetHeader) }
if (rateLimitType) { limits.rateLimitType = rateLimitType }
if (overageStatus) { limits.overageStatus = overageStatus }
const overageResetHeader = error.headers?.get?.( 'anthropic-ratelimit-unified-overage-reset', ) if (overageResetHeader) { limits.overageResetsAt = Number(overageResetHeader) }
const overageDisabledReason = error.headers?.get?.( 'anthropic-ratelimit-unified-overage-disabled-reason', ) as OverageDisabledReason | null if (overageDisabledReason) { limits.overageDisabledReason = overageDisabledReason }
// Use the new message format for all new API rate limits const specificErrorMessage = getRateLimitErrorMessage(limits, model) if (specificErrorMessage) { return createAssistantAPIErrorMessage({ content: specificErrorMessage, error: 'rate_limit', }) }
// If getRateLimitErrorMessage returned null, it means the fallback mechanism // will handle this silently (e.g., Opus -> Sonnet fallback for eligible users). // Return NO_RESPONSE_REQUESTED so no error is shown to the user, but the // message is still recorded in conversation history for Claude to see. return createAssistantAPIErrorMessage({ content: NO_RESPONSE_REQUESTED, error: 'rate_limit', }) }
// No quota headers — this is NOT a quota limit. Surface what the API actually // said instead of a generic "Rate limit reached". Entitlement rejections // (e.g. 1M context without Extra Usage) and infra capacity 429s land here. if (error.message.includes('Extra usage is required for long context')) { const hint = getIsNonInteractiveSession() ? 'enable extra usage at claude.ai/settings/usage, or use --model to switch to standard context' : 'run /extra-usage to enable, or /model to switch to standard context' return createAssistantAPIErrorMessage({ content: `${API_ERROR_MESSAGE_PREFIX}: Extra usage is required for 1M context · ${hint}`, error: 'rate_limit', }) } // SDK's APIError.makeMessage prepends "429 " and JSON-stringifies the body // when there's no top-level .message — extract the inner error.message. const stripped = error.message.replace(/^429\s+/, '') const innerMessage = stripped.match(/"message"\s*:\s*"([^"]*)"/)?.[1] const detail = innerMessage || stripped return createAssistantAPIErrorMessage({ content: `${API_ERROR_MESSAGE_PREFIX}: Request rejected (429) · ${detail || 'this may be a temporary capacity issue — check status.anthropic.com'}`, error: 'rate_limit', }) }
// Handle prompt too long errors (Vertex returns 413, direct API returns 400) // Use case-insensitive check since Vertex returns "Prompt is too long" (capitalized) if ( error instanceof Error && error.message.toLowerCase().includes('prompt is too long') ) { // Content stays generic (UI matches on exact string). The raw error with // token counts goes into errorDetails — reactive compact's retry loop // parses the gap from there via getPromptTooLongTokenGap. return createAssistantAPIErrorMessage({ content: PROMPT_TOO_LONG_ERROR_MESSAGE, error: 'invalid_request', errorDetails: error.message, }) }
// Check for PDF page limit errors if ( error instanceof Error && /maximum of \d+ PDF pages/.test(error.message) ) { return createAssistantAPIErrorMessage({ content: getPdfTooLargeErrorMessage(), error: 'invalid_request', errorDetails: error.message, }) }
// Check for password-protected PDF errors if ( error instanceof Error && error.message.includes('The PDF specified is password protected') ) { return createAssistantAPIErrorMessage({ content: getPdfPasswordProtectedErrorMessage(), error: 'invalid_request', }) }
// Check for invalid PDF errors (e.g., HTML file renamed to .pdf) // Without this handler, invalid PDF document blocks persist in conversation // context and cause every subsequent API call to fail with 400. if ( error instanceof Error && error.message.includes('The PDF specified was not valid') ) { return createAssistantAPIErrorMessage({ content: getPdfInvalidErrorMessage(), error: 'invalid_request', }) }
// Check for image size errors (e.g., "image exceeds 5 MB maximum: 5316852 bytes > 5242880 bytes") if ( error instanceof APIError && error.status === 400 && error.message.includes('image exceeds') && error.message.includes('maximum') ) { return createAssistantAPIErrorMessage({ content: getImageTooLargeErrorMessage(), errorDetails: error.message, }) }
// Check for many-image dimension errors (API enforces stricter 2000px limit for many-image requests) if ( error instanceof APIError && error.status === 400 && error.message.includes('image dimensions exceed') && error.message.includes('many-image') ) { return createAssistantAPIErrorMessage({ content: getIsNonInteractiveSession() ? 'An image in the conversation exceeds the dimension limit for many-image requests (2000px). Start a new session with fewer images.' : 'An image in the conversation exceeds the dimension limit for many-image requests (2000px). Run /compact to remove old images from context, or start a new session.', error: 'invalid_request', errorDetails: error.message, }) }
// Server rejected the afk-mode beta header (plan does not include auto // mode). AFK_MODE_BETA_HEADER is '' in non-TRANSCRIPT_CLASSIFIER builds, // so the truthy guard keeps this inert there. if ( AFK_MODE_BETA_HEADER && error instanceof APIError && error.status === 400 && error.message.includes(AFK_MODE_BETA_HEADER) && error.message.includes('anthropic-beta') ) { return createAssistantAPIErrorMessage({ content: 'Auto mode is unavailable for your plan', error: 'invalid_request', }) }
// Check for request too large errors (413 status) // This typically happens when a large PDF + conversation context exceeds the 32MB API limit if (error instanceof APIError && error.status === 413) { return createAssistantAPIErrorMessage({ content: getRequestTooLargeErrorMessage(), error: 'invalid_request', }) }
// Check for tool_use/tool_result concurrency error if ( error instanceof APIError && error.status === 400 && error.message.includes( '`tool_use` ids were found without `tool_result` blocks immediately after', ) ) { // Log to Statsig if we have the message context if (options?.messages && options?.messagesForAPI) { const toolUseIdMatch = error.message.match(/toolu_[a-zA-Z0-9]+/) const toolUseId = toolUseIdMatch ? toolUseIdMatch[0] : null if (toolUseId) { logToolUseToolResultMismatch( toolUseId, options.messages, options.messagesForAPI, ) } }
if (process.env.USER_TYPE === 'ant') { const baseMessage = `API Error: 400 ${error.message}\n\nRun /share and post the JSON file to ${MACRO.FEEDBACK_CHANNEL}.` const rewindInstruction = getIsNonInteractiveSession() ? '' : ' Then, use /rewind to recover the conversation.' return createAssistantAPIErrorMessage({ content: baseMessage + rewindInstruction, error: 'invalid_request', }) } else { const baseMessage = 'API Error: 400 due to tool use concurrency issues.' const rewindInstruction = getIsNonInteractiveSession() ? '' : ' Run /rewind to recover the conversation.' return createAssistantAPIErrorMessage({ content: baseMessage + rewindInstruction, error: 'invalid_request', }) } }
if ( error instanceof APIError && error.status === 400 && error.message.includes('unexpected `tool_use_id` found in `tool_result`') ) { logEvent('tengu_unexpected_tool_result', {}) }
// Duplicate tool_use IDs (CC-1212). ensureToolResultPairing strips these // before send, so hitting this means a new corruption path slipped through. // Log for root-causing, and give users a recovery path instead of deadlock. if ( error instanceof APIError && error.status === 400 && error.message.includes('`tool_use` ids must be unique') ) { logEvent('tengu_duplicate_tool_use_id', {}) const rewindInstruction = getIsNonInteractiveSession() ? '' : ' Run /rewind to recover the conversation.' return createAssistantAPIErrorMessage({ content: `API Error: 400 duplicate tool_use ID in conversation history.${rewindInstruction}`, error: 'invalid_request', errorDetails: error.message, }) }
// Check for invalid model name error for subscription users trying to use Opus if ( isClaudeAISubscriber() && error instanceof APIError && error.status === 400 && error.message.toLowerCase().includes('invalid model name') && (isNonCustomOpusModel(model) || model === 'opus') ) { return createAssistantAPIErrorMessage({ content: 'Claude Opus is not available with the Claude Pro plan. If you have updated your subscription plan recently, run /logout and /login for the plan to take effect.', error: 'invalid_request', }) }
// Check for invalid model name error for Ant users. Claude Code may be // defaulting to a custom internal-only model for Ants, and there might be // Ants using new or unknown org IDs that haven't been gated in. if ( process.env.USER_TYPE === 'ant' && !process.env.ANTHROPIC_MODEL && error instanceof Error && error.message.toLowerCase().includes('invalid model name') ) { // Get organization ID from config - only use OAuth account data when actively using OAuth const orgId = getOauthAccountInfo()?.organizationUuid const baseMsg = `[ANT-ONLY] Your org isn't gated into the \`${model}\` model. Either run \`claude\` with \`ANTHROPIC_MODEL=${getDefaultMainLoopModelSetting()}\`` const msg = orgId ? `${baseMsg} or share your orgId (${orgId}) in ${MACRO.FEEDBACK_CHANNEL} for help getting access.` : `${baseMsg} or reach out in ${MACRO.FEEDBACK_CHANNEL} for help getting access.`
return createAssistantAPIErrorMessage({ content: msg, error: 'invalid_request', }) }
if ( error instanceof Error && error.message.includes('Your credit balance is too low') ) { return createAssistantAPIErrorMessage({ content: CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE, error: 'billing_error', }) } // "Organization has been disabled" — commonly a stale ANTHROPIC_API_KEY // from a previous employer/project overriding subscription auth. Only handle // the env-var case; apiKeyHelper and /login-managed keys mean the active // auth's org is genuinely disabled with no dormant fallback to point at. if ( error instanceof APIError && error.status === 400 && error.message.toLowerCase().includes('organization has been disabled') ) { const { source } = getAnthropicApiKeyWithSource() // getAnthropicApiKeyWithSource conflates the env var with FD-passed keys // under the same source value, and in CCR mode OAuth stays active despite // the env var. The three guards ensure we only blame the env var when it's // actually set and actually on the wire. if ( source === 'ANTHROPIC_API_KEY' && process.env.ANTHROPIC_API_KEY && !isClaudeAISubscriber() ) { const hasStoredOAuth = getClaudeAIOAuthTokens()?.accessToken != null // Not 'authentication_failed' — that triggers VS Code's showLogin(), but // login can't fix this (approved env var keeps overriding OAuth). The fix // is configuration-based (unset the var), so invalid_request is correct. return createAssistantAPIErrorMessage({ error: 'invalid_request', content: hasStoredOAuth ? ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH : ORG_DISABLED_ERROR_MESSAGE_ENV_KEY, }) } }
if ( error instanceof Error && error.message.toLowerCase().includes('x-api-key') ) { // In CCR mode, auth is via JWTs - this is likely a transient network issue if (isCCRMode()) { return createAssistantAPIErrorMessage({ error: 'authentication_failed', content: CCR_AUTH_ERROR_MESSAGE, }) }
// Check if the API key is from an external source const { source } = getAnthropicApiKeyWithSource() const isExternalSource = source === 'ANTHROPIC_API_KEY' || source === 'apiKeyHelper'
return createAssistantAPIErrorMessage({ error: 'authentication_failed', content: isExternalSource ? INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL : INVALID_API_KEY_ERROR_MESSAGE, }) }
// Check for OAuth token revocation error if ( error instanceof APIError && error.status === 403 && error.message.includes('OAuth token has been revoked') ) { return createAssistantAPIErrorMessage({ error: 'authentication_failed', content: getTokenRevokedErrorMessage(), }) }
// Check for OAuth organization not allowed error if ( error instanceof APIError && (error.status === 401 || error.status === 403) && error.message.includes( 'OAuth authentication is currently not allowed for this organization', ) ) { return createAssistantAPIErrorMessage({ error: 'authentication_failed', content: getOauthOrgNotAllowedErrorMessage(), }) }
// Generic handler for other 401/403 authentication errors if ( error instanceof APIError && (error.status === 401 || error.status === 403) ) { // In CCR mode, auth is via JWTs - this is likely a transient network issue if (isCCRMode()) { return createAssistantAPIErrorMessage({ error: 'authentication_failed', content: CCR_AUTH_ERROR_MESSAGE, }) }
return createAssistantAPIErrorMessage({ error: 'authentication_failed', content: getIsNonInteractiveSession() ? `Failed to authenticate. ${API_ERROR_MESSAGE_PREFIX}: ${error.message}` : `Please run /login · ${API_ERROR_MESSAGE_PREFIX}: ${error.message}`, }) }
// Bedrock errors like "403 You don't have access to the model with the specified model ID." // don't contain the actual model ID if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) && error instanceof Error && error.message.toLowerCase().includes('model id') ) { const switchCmd = getIsNonInteractiveSession() ? '--model' : '/model' const fallbackSuggestion = get3PModelFallbackSuggestion(model) return createAssistantAPIErrorMessage({ content: fallbackSuggestion ? `${API_ERROR_MESSAGE_PREFIX} (${model}): ${error.message}. Try ${switchCmd} to switch to ${fallbackSuggestion}.` : `${API_ERROR_MESSAGE_PREFIX} (${model}): ${error.message}. Run ${switchCmd} to pick a different model.`, error: 'invalid_request', }) }
// 404 Not Found — usually means the selected model doesn't exist or isn't // available. Guide the user to /model so they can pick a valid one. // For 3P users, suggest a specific fallback model they can try. if (error instanceof APIError && error.status === 404) { const switchCmd = getIsNonInteractiveSession() ? '--model' : '/model' const fallbackSuggestion = get3PModelFallbackSuggestion(model) return createAssistantAPIErrorMessage({ content: fallbackSuggestion ? `The model ${model} is not available on your ${getAPIProvider()} deployment. Try ${switchCmd} to switch to ${fallbackSuggestion}, or ask your admin to enable this model.` : `There's an issue with the selected model (${model}). It may not exist or you may not have access to it. Run ${switchCmd} to pick a different model.`, error: 'invalid_request', }) }
// Connection errors (non-timeout) — use formatAPIError for detailed messages if (error instanceof APIConnectionError) { return createAssistantAPIErrorMessage({ content: `${API_ERROR_MESSAGE_PREFIX}: ${formatAPIError(error)}`, error: 'unknown', }) }
if (error instanceof Error) { return createAssistantAPIErrorMessage({ content: `${API_ERROR_MESSAGE_PREFIX}: ${error.message}`, error: 'unknown', }) } return createAssistantAPIErrorMessage({ content: API_ERROR_MESSAGE_PREFIX, error: 'unknown', })}
/** * For 3P users, suggest a fallback model when the selected model is unavailable. * Returns a model name suggestion, or undefined if no suggestion is applicable. */function get3PModelFallbackSuggestion(model: string): string | undefined { if (getAPIProvider() === 'firstParty') { return undefined } // @[MODEL LAUNCH]: Add a fallback suggestion chain for the new model → previous version for 3P const m = model.toLowerCase() // If the failing model looks like an Opus 4.6 variant, suggest the default Opus (4.1 for 3P) if (m.includes('opus-4-6') || m.includes('opus_4_6')) { return getModelStrings().opus41 } // If the failing model looks like a Sonnet 4.6 variant, suggest Sonnet 4.5 if (m.includes('sonnet-4-6') || m.includes('sonnet_4_6')) { return getModelStrings().sonnet45 } // If the failing model looks like a Sonnet 4.5 variant, suggest Sonnet 4 if (m.includes('sonnet-4-5') || m.includes('sonnet_4_5')) { return getModelStrings().sonnet40 } return undefined}
/** * Classifies an API error into a specific error type for analytics tracking. * Returns a standardized error type string suitable for Datadog tagging. */export function classifyAPIError(error: unknown): string { // Aborted requests if (error instanceof Error && error.message === 'Request was aborted.') { return 'aborted' }
// Timeout errors if ( error instanceof APIConnectionTimeoutError || (error instanceof APIConnectionError && error.message.toLowerCase().includes('timeout')) ) { return 'api_timeout' }
// Check for repeated 529 errors if ( error instanceof Error && error.message.includes(REPEATED_529_ERROR_MESSAGE) ) { return 'repeated_529' }
// Check for emergency capacity off switch if ( error instanceof Error && error.message.includes(CUSTOM_OFF_SWITCH_MESSAGE) ) { return 'capacity_off_switch' }
// Rate limiting if (error instanceof APIError && error.status === 429) { return 'rate_limit' }
// Server overload (529) if ( error instanceof APIError && (error.status === 529 || error.message?.includes('"type":"overloaded_error"')) ) { return 'server_overload' }
// Prompt/content size errors if ( error instanceof Error && error.message .toLowerCase() .includes(PROMPT_TOO_LONG_ERROR_MESSAGE.toLowerCase()) ) { return 'prompt_too_long' }
// PDF errors if ( error instanceof Error && /maximum of \d+ PDF pages/.test(error.message) ) { return 'pdf_too_large' }
if ( error instanceof Error && error.message.includes('The PDF specified is password protected') ) { return 'pdf_password_protected' }
// Image size errors if ( error instanceof APIError && error.status === 400 && error.message.includes('image exceeds') && error.message.includes('maximum') ) { return 'image_too_large' }
// Many-image dimension errors if ( error instanceof APIError && error.status === 400 && error.message.includes('image dimensions exceed') && error.message.includes('many-image') ) { return 'image_too_large' }
// Tool use errors (400) if ( error instanceof APIError && error.status === 400 && error.message.includes( '`tool_use` ids were found without `tool_result` blocks immediately after', ) ) { return 'tool_use_mismatch' }
if ( error instanceof APIError && error.status === 400 && error.message.includes('unexpected `tool_use_id` found in `tool_result`') ) { return 'unexpected_tool_result' }
if ( error instanceof APIError && error.status === 400 && error.message.includes('`tool_use` ids must be unique') ) { return 'duplicate_tool_use_id' }
// Invalid model errors (400) if ( error instanceof APIError && error.status === 400 && error.message.toLowerCase().includes('invalid model name') ) { return 'invalid_model' }
// Credit/billing errors if ( error instanceof Error && error.message .toLowerCase() .includes(CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE.toLowerCase()) ) { return 'credit_balance_low' }
// Authentication errors if ( error instanceof Error && error.message.toLowerCase().includes('x-api-key') ) { return 'invalid_api_key' }
if ( error instanceof APIError && error.status === 403 && error.message.includes('OAuth token has been revoked') ) { return 'token_revoked' }
if ( error instanceof APIError && (error.status === 401 || error.status === 403) && error.message.includes( 'OAuth authentication is currently not allowed for this organization', ) ) { return 'oauth_org_not_allowed' }
// Generic auth errors if ( error instanceof APIError && (error.status === 401 || error.status === 403) ) { return 'auth_error' }
// Bedrock-specific errors if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) && error instanceof Error && error.message.toLowerCase().includes('model id') ) { return 'bedrock_model_access' }
// Status code based fallbacks if (error instanceof APIError) { const status = error.status if (status >= 500) return 'server_error' if (status >= 400) return 'client_error' }
// Connection errors - check for SSL/TLS issues first if (error instanceof APIConnectionError) { const connectionDetails = extractConnectionErrorDetails(error) if (connectionDetails?.isSSLError) { return 'ssl_cert_error' } return 'connection_error' }
return 'unknown'}
export function categorizeRetryableAPIError( error: APIError,): SDKAssistantMessageError { if ( error.status === 529 || error.message?.includes('"type":"overloaded_error"') ) { return 'rate_limit' } if (error.status === 429) { return 'rate_limit' } if (error.status === 401 || error.status === 403) { return 'authentication_failed' } if (error.status !== undefined && error.status >= 408) { return 'server_error' } return 'unknown'}
export function getErrorMessageIfRefusal( stopReason: BetaStopReason | null, model: string,): AssistantMessage | undefined { if (stopReason !== 'refusal') { return }
logEvent('tengu_refusal_api_response', {})
const baseMessage = getIsNonInteractiveSession() ? `${API_ERROR_MESSAGE_PREFIX}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (https://www.anthropic.com/legal/aup). Try rephrasing the request or attempting a different approach.` : `${API_ERROR_MESSAGE_PREFIX}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (https://www.anthropic.com/legal/aup). Please double press esc to edit your last message or start a new session for Claude Code to assist with a different task.`
const modelSuggestion = model !== 'claude-sonnet-4-20250514' ? ' If you are seeing this refusal repeatedly, try running /model claude-sonnet-4-20250514 to switch models.' : ''
return createAssistantAPIErrorMessage({ content: baseMessage + modelSuggestion, error: 'invalid_request', })}