@animalabs/membrane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/context/index.d.ts +10 -0
  2. package/dist/context/index.d.ts.map +1 -0
  3. package/dist/context/index.js +9 -0
  4. package/dist/context/index.js.map +1 -0
  5. package/dist/context/process.d.ts +22 -0
  6. package/dist/context/process.d.ts.map +1 -0
  7. package/dist/context/process.js +369 -0
  8. package/dist/context/process.js.map +1 -0
  9. package/dist/context/types.d.ts +118 -0
  10. package/dist/context/types.d.ts.map +1 -0
  11. package/dist/context/types.js +60 -0
  12. package/dist/context/types.js.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +18 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/membrane.d.ts +96 -0
  18. package/dist/membrane.d.ts.map +1 -0
  19. package/dist/membrane.js +893 -0
  20. package/dist/membrane.js.map +1 -0
  21. package/dist/providers/anthropic.d.ts +36 -0
  22. package/dist/providers/anthropic.d.ts.map +1 -0
  23. package/dist/providers/anthropic.js +265 -0
  24. package/dist/providers/anthropic.js.map +1 -0
  25. package/dist/providers/index.d.ts +8 -0
  26. package/dist/providers/index.d.ts.map +1 -0
  27. package/dist/providers/index.js +8 -0
  28. package/dist/providers/index.js.map +1 -0
  29. package/dist/providers/openai-compatible.d.ts +74 -0
  30. package/dist/providers/openai-compatible.d.ts.map +1 -0
  31. package/dist/providers/openai-compatible.js +412 -0
  32. package/dist/providers/openai-compatible.js.map +1 -0
  33. package/dist/providers/openai.d.ts +69 -0
  34. package/dist/providers/openai.d.ts.map +1 -0
  35. package/dist/providers/openai.js +455 -0
  36. package/dist/providers/openai.js.map +1 -0
  37. package/dist/providers/openrouter.d.ts +76 -0
  38. package/dist/providers/openrouter.d.ts.map +1 -0
  39. package/dist/providers/openrouter.js +492 -0
  40. package/dist/providers/openrouter.js.map +1 -0
  41. package/dist/transforms/chat.d.ts +52 -0
  42. package/dist/transforms/chat.d.ts.map +1 -0
  43. package/dist/transforms/chat.js +136 -0
  44. package/dist/transforms/chat.js.map +1 -0
  45. package/dist/transforms/index.d.ts +6 -0
  46. package/dist/transforms/index.d.ts.map +1 -0
  47. package/dist/transforms/index.js +6 -0
  48. package/dist/transforms/index.js.map +1 -0
  49. package/dist/transforms/prefill.d.ts +89 -0
  50. package/dist/transforms/prefill.d.ts.map +1 -0
  51. package/dist/transforms/prefill.js +401 -0
  52. package/dist/transforms/prefill.js.map +1 -0
  53. package/dist/types/config.d.ts +103 -0
  54. package/dist/types/config.d.ts.map +1 -0
  55. package/dist/types/config.js +21 -0
  56. package/dist/types/config.js.map +1 -0
  57. package/dist/types/content.d.ts +81 -0
  58. package/dist/types/content.d.ts.map +1 -0
  59. package/dist/types/content.js +40 -0
  60. package/dist/types/content.js.map +1 -0
  61. package/dist/types/errors.d.ts +42 -0
  62. package/dist/types/errors.d.ts.map +1 -0
  63. package/dist/types/errors.js +208 -0
  64. package/dist/types/errors.js.map +1 -0
  65. package/dist/types/index.d.ts +18 -0
  66. package/dist/types/index.d.ts.map +1 -0
  67. package/dist/types/index.js +9 -0
  68. package/dist/types/index.js.map +1 -0
  69. package/dist/types/message.d.ts +46 -0
  70. package/dist/types/message.d.ts.map +1 -0
  71. package/dist/types/message.js +38 -0
  72. package/dist/types/message.js.map +1 -0
  73. package/dist/types/provider.d.ts +155 -0
  74. package/dist/types/provider.d.ts.map +1 -0
  75. package/dist/types/provider.js +5 -0
  76. package/dist/types/provider.js.map +1 -0
  77. package/dist/types/request.d.ts +78 -0
  78. package/dist/types/request.d.ts.map +1 -0
  79. package/dist/types/request.js +5 -0
  80. package/dist/types/request.js.map +1 -0
  81. package/dist/types/response.d.ts +131 -0
  82. package/dist/types/response.d.ts.map +1 -0
  83. package/dist/types/response.js +7 -0
  84. package/dist/types/response.js.map +1 -0
  85. package/dist/types/streaming.d.ts +164 -0
  86. package/dist/types/streaming.d.ts.map +1 -0
  87. package/dist/types/streaming.js +5 -0
  88. package/dist/types/streaming.js.map +1 -0
  89. package/dist/types/tools.d.ts +71 -0
  90. package/dist/types/tools.d.ts.map +1 -0
  91. package/dist/types/tools.js +5 -0
  92. package/dist/types/tools.js.map +1 -0
  93. package/dist/utils/index.d.ts +5 -0
  94. package/dist/utils/index.d.ts.map +1 -0
  95. package/dist/utils/index.js +5 -0
  96. package/dist/utils/index.js.map +1 -0
  97. package/dist/utils/stream-parser.d.ts +53 -0
  98. package/dist/utils/stream-parser.d.ts.map +1 -0
  99. package/dist/utils/stream-parser.js +359 -0
  100. package/dist/utils/stream-parser.js.map +1 -0
  101. package/dist/utils/tool-parser.d.ts +130 -0
  102. package/dist/utils/tool-parser.d.ts.map +1 -0
  103. package/dist/utils/tool-parser.js +571 -0
  104. package/dist/utils/tool-parser.js.map +1 -0
  105. package/package.json +37 -0
  106. package/src/context/index.ts +24 -0
  107. package/src/context/process.ts +520 -0
  108. package/src/context/types.ts +231 -0
  109. package/src/index.ts +23 -0
  110. package/src/membrane.ts +1174 -0
  111. package/src/providers/anthropic.ts +340 -0
  112. package/src/providers/index.ts +31 -0
  113. package/src/providers/openai-compatible.ts +570 -0
  114. package/src/providers/openai.ts +625 -0
  115. package/src/providers/openrouter.ts +662 -0
  116. package/src/transforms/chat.ts +212 -0
  117. package/src/transforms/index.ts +22 -0
  118. package/src/transforms/prefill.ts +585 -0
  119. package/src/types/config.ts +172 -0
  120. package/src/types/content.ts +181 -0
  121. package/src/types/errors.ts +277 -0
  122. package/src/types/index.ts +154 -0
  123. package/src/types/message.ts +89 -0
  124. package/src/types/provider.ts +249 -0
  125. package/src/types/request.ts +131 -0
  126. package/src/types/response.ts +223 -0
  127. package/src/types/streaming.ts +231 -0
  128. package/src/types/tools.ts +92 -0
  129. package/src/utils/index.ts +15 -0
  130. package/src/utils/stream-parser.ts +440 -0
  131. package/src/utils/tool-parser.ts +715 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Configuration types for membrane
3
+ */
4
+
5
+ import type { ModelRegistry } from './provider.js';
6
+ import type { ErrorInfo } from './errors.js';
7
+ import type { NormalizedRequest } from './request.js';
8
+ import type { NormalizedResponse } from './response.js';
9
+
10
+ // ============================================================================
11
+ // Retry Config
12
+ // ============================================================================
13
+
14
+ export interface RetryConfig {
15
+ /** Maximum number of retry attempts (default: 3) */
16
+ maxRetries: number;
17
+
18
+ /** Initial retry delay in milliseconds (default: 1000) */
19
+ retryDelayMs: number;
20
+
21
+ /** Backoff multiplier (default: 2) */
22
+ backoffMultiplier: number;
23
+
24
+ /** Maximum retry delay (default: 30000) */
25
+ maxRetryDelayMs: number;
26
+ }
27
+
28
+ // ============================================================================
29
+ // Media Processing Config
30
+ // ============================================================================
31
+
32
+ export interface MediaConfig {
33
+ images: {
34
+ /** Maximum input image size in bytes */
35
+ maxSizeBytes: number;
36
+
37
+ /** Maximum dimensions */
38
+ maxDimensions?: { width: number; height: number };
39
+
40
+ /** Auto-resize if exceeds limits */
41
+ autoResize: boolean;
42
+
43
+ /** JPEG quality for resizing (0-100) */
44
+ resizeQuality?: number;
45
+
46
+ /** Relocate images to user turns in prefill mode */
47
+ relocateInPrefillMode: boolean;
48
+ };
49
+
50
+ documents?: {
51
+ /** Maximum document size */
52
+ maxSizeBytes: number;
53
+ };
54
+
55
+ audio?: {
56
+ /** Maximum duration in seconds */
57
+ maxDurationSec: number;
58
+ };
59
+
60
+ video?: {
61
+ /** Maximum duration in seconds */
62
+ maxDurationSec: number;
63
+ };
64
+ }
65
+
66
+ // ============================================================================
67
+ // Hooks
68
+ // ============================================================================
69
+
70
+ export interface MembraneHooks {
71
+ /**
72
+ * Called before sending request to provider
73
+ * Can modify the raw request
74
+ */
75
+ beforeRequest?: (
76
+ request: NormalizedRequest,
77
+ rawRequest: unknown
78
+ ) => unknown | Promise<unknown>;
79
+
80
+ /**
81
+ * Called after receiving response from provider
82
+ * Can modify the response
83
+ */
84
+ afterResponse?: (
85
+ response: NormalizedResponse,
86
+ rawResponse: unknown
87
+ ) => NormalizedResponse | Promise<NormalizedResponse>;
88
+
89
+ /**
90
+ * Called on error, before retry decision
91
+ * Return 'retry' to retry, 'abort' to stop
92
+ */
93
+ onError?: (
94
+ error: ErrorInfo,
95
+ attempt: number
96
+ ) => 'retry' | 'abort' | Promise<'retry' | 'abort'>;
97
+ }
98
+
99
+ // ============================================================================
100
+ // Logger Interface
101
+ // ============================================================================
102
+
103
+ export interface MembraneLogger {
104
+ debug(message: string, data?: Record<string, unknown>): void;
105
+ info(message: string, data?: Record<string, unknown>): void;
106
+ warn(message: string, data?: Record<string, unknown>): void;
107
+ error(message: string, data?: Record<string, unknown>): void;
108
+ }
109
+
110
+ // ============================================================================
111
+ // Membrane Config
112
+ // ============================================================================
113
+
114
+ export interface MembraneConfig {
115
+ /** Model registry for capability lookup */
116
+ registry?: ModelRegistry;
117
+
118
+ /** Default model to use */
119
+ defaultModel?: string;
120
+
121
+ /**
122
+ * Participant name to recognize as assistant in prefill mode.
123
+ * Messages with this participant will be formatted as assistant turns.
124
+ * Default: 'Claude'
125
+ */
126
+ assistantParticipant?: string;
127
+
128
+ /**
129
+ * Maximum number of participants to include in auto-generated stop sequences.
130
+ * In prefill mode, membrane generates stop sequences like "\nUsername:" to prevent
131
+ * the model from speaking as other participants.
132
+ *
133
+ * Set to 0 to disable participant-based stop sequences (allows frags/quotes).
134
+ * Default: 10
135
+ */
136
+ maxParticipantsForStop?: number;
137
+
138
+ /** Retry configuration */
139
+ retry?: Partial<RetryConfig>;
140
+
141
+ /** Media processing configuration */
142
+ media?: Partial<MediaConfig>;
143
+
144
+ /** Lifecycle hooks */
145
+ hooks?: MembraneHooks;
146
+
147
+ /** Logger instance */
148
+ logger?: MembraneLogger;
149
+
150
+ /** Enable debug logging */
151
+ debug?: boolean;
152
+ }
153
+
154
+ // ============================================================================
155
+ // Default Config
156
+ // ============================================================================
157
+
158
+ export const DEFAULT_RETRY_CONFIG: RetryConfig = {
159
+ maxRetries: 3,
160
+ retryDelayMs: 1000,
161
+ backoffMultiplier: 2,
162
+ maxRetryDelayMs: 30000,
163
+ };
164
+
165
+ export const DEFAULT_MEDIA_CONFIG: MediaConfig = {
166
+ images: {
167
+ maxSizeBytes: 5 * 1024 * 1024, // 5MB (Anthropic limit)
168
+ autoResize: true,
169
+ resizeQuality: 85,
170
+ relocateInPrefillMode: true,
171
+ },
172
+ };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Content block types for normalized messages
3
+ */
4
+
5
+ // ============================================================================
6
+ // Cache Control (Anthropic prompt caching)
7
+ // ============================================================================
8
+
9
+ export interface CacheControl {
10
+ type: 'ephemeral';
11
+ }
12
+
13
+ // ============================================================================
14
+ // Media Source
15
+ // ============================================================================
16
+
17
+ export interface Base64Source {
18
+ type: 'base64';
19
+ data: string;
20
+ mediaType: string;
21
+ }
22
+
23
+ export interface UrlSource {
24
+ type: 'url';
25
+ url: string;
26
+ }
27
+
28
+ export type MediaSource = Base64Source | UrlSource;
29
+
30
+ // ============================================================================
31
+ // Text Content
32
+ // ============================================================================
33
+
34
+ export interface TextContent {
35
+ type: 'text';
36
+ text: string;
37
+ /** Cache control for Anthropic prompt caching */
38
+ cache_control?: CacheControl;
39
+ }
40
+
41
+ // ============================================================================
42
+ // Media Input Content
43
+ // ============================================================================
44
+
45
+ export interface ImageContent {
46
+ type: 'image';
47
+ source: MediaSource;
48
+ tokenEstimate?: number;
49
+ }
50
+
51
+ export interface DocumentContent {
52
+ type: 'document';
53
+ source: Base64Source;
54
+ filename?: string;
55
+ }
56
+
57
+ export interface AudioContent {
58
+ type: 'audio';
59
+ source: Base64Source;
60
+ duration?: number; // seconds
61
+ }
62
+
63
+ export interface VideoContent {
64
+ type: 'video';
65
+ source: Base64Source;
66
+ duration?: number; // seconds
67
+ }
68
+
69
+ // ============================================================================
70
+ // Media Output Content (Generated)
71
+ // ============================================================================
72
+
73
+ export interface GeneratedImageContent {
74
+ type: 'generated_image';
75
+ data: string;
76
+ mimeType: string;
77
+ isPreview?: boolean; // Streaming: preview vs final
78
+ }
79
+
80
+ // ============================================================================
81
+ // Tool Content
82
+ // ============================================================================
83
+
84
+ export interface ToolUseContent {
85
+ type: 'tool_use';
86
+ id: string;
87
+ name: string;
88
+ input: Record<string, unknown>;
89
+ }
90
+
91
+ export interface ToolResultContent {
92
+ type: 'tool_result';
93
+ toolUseId: string;
94
+ content: string | ContentBlock[];
95
+ isError?: boolean;
96
+ }
97
+
98
+ // ============================================================================
99
+ // Thinking Content
100
+ // ============================================================================
101
+
102
+ export interface ThinkingContent {
103
+ type: 'thinking';
104
+ thinking: string;
105
+ signature?: string;
106
+ }
107
+
108
+ export interface RedactedThinkingContent {
109
+ type: 'redacted_thinking';
110
+ }
111
+
112
+ // ============================================================================
113
+ // Union Type
114
+ // ============================================================================
115
+
116
+ export type ContentBlock =
117
+ // Text
118
+ | TextContent
119
+ // Media Input
120
+ | ImageContent
121
+ | DocumentContent
122
+ | AudioContent
123
+ | VideoContent
124
+ // Media Output
125
+ | GeneratedImageContent
126
+ // Tools
127
+ | ToolUseContent
128
+ | ToolResultContent
129
+ // Thinking
130
+ | ThinkingContent
131
+ | RedactedThinkingContent;
132
+
133
+ // ============================================================================
134
+ // Type Guards
135
+ // ============================================================================
136
+
137
+ export function isTextContent(block: ContentBlock): block is TextContent {
138
+ return block.type === 'text';
139
+ }
140
+
141
+ export function isImageContent(block: ContentBlock): block is ImageContent {
142
+ return block.type === 'image';
143
+ }
144
+
145
+ export function isDocumentContent(block: ContentBlock): block is DocumentContent {
146
+ return block.type === 'document';
147
+ }
148
+
149
+ export function isAudioContent(block: ContentBlock): block is AudioContent {
150
+ return block.type === 'audio';
151
+ }
152
+
153
+ export function isVideoContent(block: ContentBlock): block is VideoContent {
154
+ return block.type === 'video';
155
+ }
156
+
157
+ export function isGeneratedImageContent(block: ContentBlock): block is GeneratedImageContent {
158
+ return block.type === 'generated_image';
159
+ }
160
+
161
+ export function isToolUseContent(block: ContentBlock): block is ToolUseContent {
162
+ return block.type === 'tool_use';
163
+ }
164
+
165
+ export function isToolResultContent(block: ContentBlock): block is ToolResultContent {
166
+ return block.type === 'tool_result';
167
+ }
168
+
169
+ export function isThinkingContent(block: ContentBlock): block is ThinkingContent {
170
+ return block.type === 'thinking';
171
+ }
172
+
173
+ export function isRedactedThinkingContent(block: ContentBlock): block is RedactedThinkingContent {
174
+ return block.type === 'redacted_thinking';
175
+ }
176
+
177
+ export function isMediaContent(
178
+ block: ContentBlock
179
+ ): block is ImageContent | DocumentContent | AudioContent | VideoContent {
180
+ return ['image', 'document', 'audio', 'video'].includes(block.type);
181
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Error types for membrane
3
+ */
4
+
5
+ // ============================================================================
6
+ // Error Types
7
+ // ============================================================================
8
+
9
+ export type MembraneErrorType =
10
+ | 'rate_limit'
11
+ | 'context_length'
12
+ | 'invalid_request'
13
+ | 'auth'
14
+ | 'server'
15
+ | 'network'
16
+ | 'timeout'
17
+ | 'abort'
18
+ | 'safety'
19
+ | 'unsupported'
20
+ | 'unknown';
21
+
22
+ // ============================================================================
23
+ // Error Info (for hooks and logging)
24
+ // ============================================================================
25
+
26
+ export interface ErrorInfo {
27
+ /** Normalized error type */
28
+ type: MembraneErrorType;
29
+
30
+ /** Human-readable message */
31
+ message: string;
32
+
33
+ /** Whether this error is retryable */
34
+ retryable: boolean;
35
+
36
+ /** Retry after (milliseconds) - for rate limits */
37
+ retryAfterMs?: number;
38
+
39
+ /** HTTP status code if available */
40
+ httpStatus?: number;
41
+
42
+ /** Provider-specific error code */
43
+ providerErrorCode?: string;
44
+
45
+ /** Raw error object */
46
+ rawError: unknown;
47
+ }
48
+
49
+ // ============================================================================
50
+ // Membrane Error Class
51
+ // ============================================================================
52
+
53
+ export class MembraneError extends Error {
54
+ readonly type: MembraneErrorType;
55
+ readonly retryable: boolean;
56
+ readonly retryAfterMs?: number;
57
+ readonly httpStatus?: number;
58
+ readonly providerErrorCode?: string;
59
+ readonly rawError?: unknown;
60
+
61
+ constructor(info: ErrorInfo) {
62
+ super(info.message);
63
+ this.name = 'MembraneError';
64
+ this.type = info.type;
65
+ this.retryable = info.retryable;
66
+ this.retryAfterMs = info.retryAfterMs;
67
+ this.httpStatus = info.httpStatus;
68
+ this.providerErrorCode = info.providerErrorCode;
69
+ this.rawError = info.rawError;
70
+ }
71
+
72
+ toErrorInfo(): ErrorInfo {
73
+ return {
74
+ type: this.type,
75
+ message: this.message,
76
+ retryable: this.retryable,
77
+ retryAfterMs: this.retryAfterMs,
78
+ httpStatus: this.httpStatus,
79
+ providerErrorCode: this.providerErrorCode,
80
+ rawError: this.rawError,
81
+ };
82
+ }
83
+ }
84
+
85
+ // ============================================================================
86
+ // Error Factory Functions
87
+ // ============================================================================
88
+
89
+ export function rateLimitError(message: string, retryAfterMs?: number, raw?: unknown): MembraneError {
90
+ return new MembraneError({
91
+ type: 'rate_limit',
92
+ message,
93
+ retryable: true,
94
+ retryAfterMs,
95
+ httpStatus: 429,
96
+ rawError: raw,
97
+ });
98
+ }
99
+
100
+ export function contextLengthError(message: string, raw?: unknown): MembraneError {
101
+ return new MembraneError({
102
+ type: 'context_length',
103
+ message,
104
+ retryable: false,
105
+ httpStatus: 400,
106
+ rawError: raw,
107
+ });
108
+ }
109
+
110
+ export function invalidRequestError(message: string, raw?: unknown): MembraneError {
111
+ return new MembraneError({
112
+ type: 'invalid_request',
113
+ message,
114
+ retryable: false,
115
+ httpStatus: 400,
116
+ rawError: raw,
117
+ });
118
+ }
119
+
120
+ export function authError(message: string, raw?: unknown): MembraneError {
121
+ return new MembraneError({
122
+ type: 'auth',
123
+ message,
124
+ retryable: false,
125
+ httpStatus: 401,
126
+ rawError: raw,
127
+ });
128
+ }
129
+
130
+ export function serverError(message: string, httpStatus?: number, raw?: unknown): MembraneError {
131
+ return new MembraneError({
132
+ type: 'server',
133
+ message,
134
+ retryable: true,
135
+ httpStatus: httpStatus ?? 500,
136
+ rawError: raw,
137
+ });
138
+ }
139
+
140
+ export function networkError(message: string, raw?: unknown): MembraneError {
141
+ return new MembraneError({
142
+ type: 'network',
143
+ message,
144
+ retryable: true,
145
+ rawError: raw,
146
+ });
147
+ }
148
+
149
+ export function timeoutError(message: string, raw?: unknown): MembraneError {
150
+ return new MembraneError({
151
+ type: 'timeout',
152
+ message,
153
+ retryable: true,
154
+ rawError: raw,
155
+ });
156
+ }
157
+
158
+ export function abortError(message: string = 'Request was aborted'): MembraneError {
159
+ return new MembraneError({
160
+ type: 'abort',
161
+ message,
162
+ retryable: false,
163
+ rawError: undefined,
164
+ });
165
+ }
166
+
167
+ export function safetyError(message: string, raw?: unknown): MembraneError {
168
+ return new MembraneError({
169
+ type: 'safety',
170
+ message,
171
+ retryable: false,
172
+ rawError: raw,
173
+ });
174
+ }
175
+
176
+ export function unsupportedError(message: string): MembraneError {
177
+ return new MembraneError({
178
+ type: 'unsupported',
179
+ message,
180
+ retryable: false,
181
+ rawError: undefined,
182
+ });
183
+ }
184
+
185
+ // ============================================================================
186
+ // Error Classification
187
+ // ============================================================================
188
+
189
+ export function classifyError(error: unknown): ErrorInfo {
190
+ if (error instanceof MembraneError) {
191
+ return error.toErrorInfo();
192
+ }
193
+
194
+ if (error instanceof Error) {
195
+ const message = error.message.toLowerCase();
196
+
197
+ // Rate limit
198
+ if (message.includes('rate') || message.includes('429') || message.includes('too many')) {
199
+ return {
200
+ type: 'rate_limit',
201
+ message: error.message,
202
+ retryable: true,
203
+ httpStatus: 429,
204
+ rawError: error,
205
+ };
206
+ }
207
+
208
+ // Context length
209
+ if (message.includes('context') || message.includes('too long') || message.includes('maximum')) {
210
+ return {
211
+ type: 'context_length',
212
+ message: error.message,
213
+ retryable: false,
214
+ rawError: error,
215
+ };
216
+ }
217
+
218
+ // Auth
219
+ if (message.includes('auth') || message.includes('401') || message.includes('api key')) {
220
+ return {
221
+ type: 'auth',
222
+ message: error.message,
223
+ retryable: false,
224
+ httpStatus: 401,
225
+ rawError: error,
226
+ };
227
+ }
228
+
229
+ // Network
230
+ if (message.includes('network') || message.includes('econnreset') || message.includes('socket')) {
231
+ return {
232
+ type: 'network',
233
+ message: error.message,
234
+ retryable: true,
235
+ rawError: error,
236
+ };
237
+ }
238
+
239
+ // Timeout
240
+ if (message.includes('timeout') || message.includes('timed out')) {
241
+ return {
242
+ type: 'timeout',
243
+ message: error.message,
244
+ retryable: true,
245
+ rawError: error,
246
+ };
247
+ }
248
+
249
+ // Abort
250
+ if (message.includes('abort') || error.name === 'AbortError') {
251
+ return {
252
+ type: 'abort',
253
+ message: error.message,
254
+ retryable: false,
255
+ rawError: error,
256
+ };
257
+ }
258
+
259
+ // Server error
260
+ if (message.includes('500') || message.includes('502') || message.includes('503') || message.includes('504')) {
261
+ return {
262
+ type: 'server',
263
+ message: error.message,
264
+ retryable: true,
265
+ rawError: error,
266
+ };
267
+ }
268
+ }
269
+
270
+ // Unknown
271
+ return {
272
+ type: 'unknown',
273
+ message: error instanceof Error ? error.message : String(error),
274
+ retryable: false,
275
+ rawError: error,
276
+ };
277
+ }