diff --git a/packages/@anthropic-ai/model-provider/src/errorUtils.ts b/packages/@anthropic-ai/model-provider/src/errorUtils.ts new file mode 100644 index 000000000..c59511145 --- /dev/null +++ b/packages/@anthropic-ai/model-provider/src/errorUtils.ts @@ -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('([^<]+)<\/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 +} diff --git a/src/QueryEngine.ts b/src/QueryEngine.ts index c9d67d382..1266d68d4 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -16,8 +16,8 @@ import type { } from 'src/entrypoints/agentSdkTypes.js' import type { BetaMessageDeltaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' import { accumulateUsage, updateUsage } from 'src/services/api/claude.js' -import type { NonNullableUsage } from 'src/services/api/logging.js' -import { EMPTY_USAGE } from 'src/services/api/logging.js' +import type { NonNullableUsage } from '@anthropic-ai/model-provider' +import { EMPTY_USAGE } from '@anthropic-ai/model-provider' import stripAnsi from 'strip-ansi' import type { Command } from './commands.js' import { getSlashCommandToolSkills } from './commands.js' diff --git a/src/bridge/bridgeMessaging.ts b/src/bridge/bridgeMessaging.ts index eab387fb8..d0f48d82a 100644 --- a/src/bridge/bridgeMessaging.ts +++ b/src/bridge/bridgeMessaging.ts @@ -18,7 +18,7 @@ import type { } from '../entrypoints/sdk/controlTypes.js' import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.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 { normalizeControlMessageKeys } from '../utils/controlMessageCompat.js' import { logForDebugging } from '../utils/debug.js' diff --git a/src/cli/handlers/auth.ts b/src/cli/handlers/auth.ts index 8b92c7dde..9e6b94a85 100644 --- a/src/cli/handlers/auth.ts +++ b/src/cli/handlers/auth.ts @@ -8,7 +8,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } 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 { createAndStoreApiKey, diff --git a/src/cli/print.ts b/src/cli/print.ts index 5f2cb308c..d0a7dd37f 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -65,7 +65,7 @@ import { registerProcessOutputErrorHandlers, } from 'src/utils/process.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 { loadConversationForResume, type TurnInterruptionState, diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index bd1dd5d1e..4a2707e44 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -7,7 +7,7 @@ import { installOAuthTokens } from '../cli/handlers/auth.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' import { setClipboard, useTerminalNotification, Box, Link, Text, KeyboardShortcutHint } from '@anthropic/ink' 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 { OAuthService } from '../services/oauth/index.js' import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js' diff --git a/src/components/messages/SystemAPIErrorMessage.tsx b/src/components/messages/SystemAPIErrorMessage.tsx index 4efa74750..364146f90 100644 --- a/src/components/messages/SystemAPIErrorMessage.tsx +++ b/src/components/messages/SystemAPIErrorMessage.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useState } from 'react' 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 { useInterval } from 'usehooks-ts' import { CtrlOToExpand } from '../CtrlOToExpand.js' diff --git a/src/entrypoints/sdk/sdkUtilityTypes.ts b/src/entrypoints/sdk/sdkUtilityTypes.ts index ecaa32de0..9e965733f 100644 --- a/src/entrypoints/sdk/sdkUtilityTypes.ts +++ b/src/entrypoints/sdk/sdkUtilityTypes.ts @@ -1,24 +1,5 @@ /** * Stub: SDK Utility Types. + * Re-exported from @anthropic-ai/model-provider. */ -export type NonNullableUsage = { - 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 -} +export type { NonNullableUsage } from '@anthropic-ai/model-provider' diff --git a/src/services/api/emptyUsage.ts b/src/services/api/emptyUsage.ts index ad8c25ffd..7514f2813 100644 --- a/src/services/api/emptyUsage.ts +++ b/src/services/api/emptyUsage.ts @@ -1,22 +1,4 @@ -import type { NonNullableUsage } from '../../entrypoints/sdk/sdkUtilityTypes.js' - -/** - * Zero-initialized usage object. Extracted from logging.ts so that - * 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 = { - 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', -} +// 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' +export type { NonNullableUsage } from '@anthropic-ai/model-provider' diff --git a/src/services/api/errorUtils.ts b/src/services/api/errorUtils.ts index 20e4441f1..521b2d9e7 100644 --- a/src/services/api/errorUtils.ts +++ b/src/services/api/errorUtils.ts @@ -1,260 +1,8 @@ -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. - * - * 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('([^<]+)<\/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 -} +// Re-export from @anthropic-ai/model-provider +export { + formatAPIError, + extractConnectionErrorDetails, + sanitizeAPIError, + getSSLErrorHint, + type ConnectionErrorDetails, +} from '@anthropic-ai/model-provider' diff --git a/src/types/message.ts b/src/types/message.ts index 567bae475..49159c70b 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,141 +1,74 @@ -// Auto-generated stub — replace with real implementation -import type { UUID } from 'crypto' -import type { - ContentBlockParam, - ContentBlock, -} from '@anthropic-ai/sdk/resources/index.mjs' -import type { BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +// Re-export core message types from @anthropic-ai/model-provider +// This file adds UI-specific types on top of the base types. +export type { + MessageType, + ContentItem, + MessageContent, + 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 { BranchAction, CommitKind, PrAction, } from '@claude-code-best/builtin-tools/tools/shared/gitOperationTracking.js' - -/** - * Base message type with discriminant `type` field and common properties. - * Individual message subtypes (UserMessage, AssistantMessage, etc.) extend - * this with narrower `type` literals and additional fields. - */ -export type MessageType = 'user' | 'assistant' | 'system' | 'attachment' | 'progress' | 'grouped_tool_use' | 'collapsed_read_search' - -/** A single content element inside message.content arrays. */ -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 - [key: string]: unknown - } - [key: string]: unknown -} - -export type AssistantMessage = Message & { - type: 'assistant' - message: NonNullable -} -export type AttachmentMessage = Message & { type: 'attachment'; attachment: T } -export type ProgressMessage = 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 - 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 -export type SystemAPIErrorMessage = Message & { type: 'system' } -export type SystemFileSnapshotMessage = Message & { type: 'system' } -export type NormalizedAssistantMessage = 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 -} +import type { + AssistantMessage, + CollapsibleMessage, + NormalizedAssistantMessage, + NormalizedUserMessage, + UserMessage, +} from '@anthropic-ai/model-provider' +import type { UUID } from 'crypto' +import type { StopHookInfo } from '@anthropic-ai/model-provider' export type RenderableMessage = | AssistantMessage | UserMessage - | (Message & { type: 'system' }) - | (Message & { type: 'attachment'; attachment: { type: string; memories?: { path: string; content: string; mtimeMs: number }[]; [key: string]: unknown } }) - | (Message & { type: 'progress' }) - | GroupedToolUseMessage + | (import('@anthropic-ai/model-provider').Message & { type: 'system' }) + | (import('@anthropic-ai/model-provider').Message & { type: 'attachment'; attachment: { type: string; memories?: { path: string; content: string; mtimeMs: number }[]; [key: string]: unknown } }) + | (import('@anthropic-ai/model-provider').Message & { type: 'progress' }) + | import('@anthropic-ai/model-provider').GroupedToolUseMessage | CollapsedReadSearchGroup -export type CollapsibleMessage = - | AssistantMessage - | UserMessage - | GroupedToolUseMessage - export type CollapsedReadSearchGroup = { type: 'collapsed_read_search' uuid: UUID @@ -169,6 +102,3 @@ export type CollapsedReadSearchGroup = { teamMemoryWriteCount?: number [key: string]: unknown } - -export type HookResultMessage = Message -export type SystemThinkingMessage = Message & { type: 'system' } diff --git a/src/utils/forkedAgent.ts b/src/utils/forkedAgent.ts index c6717b1ff..efeefdfe8 100644 --- a/src/utils/forkedAgent.ts +++ b/src/utils/forkedAgent.ts @@ -19,7 +19,7 @@ import { logEvent, } from '../services/analytics/index.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 { AgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js' import type { AgentId } from '../types/ids.js' diff --git a/src/utils/sideQuestion.ts b/src/utils/sideQuestion.ts index 4d2755e20..305a12e58 100644 --- a/src/utils/sideQuestion.ts +++ b/src/utils/sideQuestion.ts @@ -6,8 +6,8 @@ * while keeping the side question response separate from main conversation. */ -import { formatAPIError } from '../services/api/errorUtils.js' -import type { NonNullableUsage } from '../services/api/logging.js' +import { formatAPIError } from '@anthropic-ai/model-provider' +import type { NonNullableUsage } from '@anthropic-ai/model-provider' import type { Message, SystemAPIErrorMessage } from '../types/message.js' import { type CacheSafeParams, runForkedAgent } from './forkedAgent.js' import { createUserMessage, extractTextContent } from './messages.js' diff --git a/src/utils/systemPromptType.ts b/src/utils/systemPromptType.ts index c3efa6e6a..0b6f7f538 100644 --- a/src/utils/systemPromptType.ts +++ b/src/utils/systemPromptType.ts @@ -1,14 +1,4 @@ -/** - * Branded type for system prompt arrays. - * - * This module is intentionally dependency-free so it can be imported - * 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 -} +// Re-export SystemPrompt from @anthropic-ai/model-provider +// Kept here for backward compatibility. +export type { SystemPrompt } from '@anthropic-ai/model-provider' +export { asSystemPrompt } from '@anthropic-ai/model-provider'