@animalabs/membrane 0.3.0 → 0.3.1

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.
@@ -2,8 +2,6 @@
2
2
  * Provider exports
3
3
  */
4
4
  export { AnthropicAdapter, toAnthropicContent, fromAnthropicContent, type AnthropicAdapterConfig, } from './anthropic.js';
5
- export { AnthropicChatAdapter, type AnthropicChatAdapterConfig, } from './anthropic-chat.js';
6
- export { AnthropicMultiuserAdapter, type AnthropicMultiuserAdapterConfig, } from './anthropic-multiuser.js';
7
5
  export { OpenRouterAdapter, toOpenRouterMessages, fromOpenRouterMessage, type OpenRouterAdapterConfig, } from './openrouter.js';
8
6
  export { OpenAIAdapter, toOpenAIContent, fromOpenAIContent, type OpenAIAdapterConfig, } from './openai.js';
9
7
  export { OpenAICompatibleAdapter, toOpenAIMessages, fromOpenAIMessage, type OpenAICompatibleAdapterConfig, } from './openai-compatible.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,oBAAoB,EACpB,KAAK,0BAA0B,GAChC,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,yBAAyB,EACzB,KAAK,+BAA+B,GACrC,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,6BAA6B,GACnC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,wBAAwB,EACxB,KAAK,8BAA8B,GACpC,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,6BAA6B,GACnC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,wBAAwB,EACxB,KAAK,8BAA8B,GACpC,MAAM,yBAAyB,CAAC"}
@@ -2,8 +2,6 @@
2
2
  * Provider exports
3
3
  */
4
4
  export { AnthropicAdapter, toAnthropicContent, fromAnthropicContent, } from './anthropic.js';
5
- export { AnthropicChatAdapter, } from './anthropic-chat.js';
6
- export { AnthropicMultiuserAdapter, } from './anthropic-multiuser.js';
7
5
  export { OpenRouterAdapter, toOpenRouterMessages, fromOpenRouterMessage, } from './openrouter.js';
8
6
  export { OpenAIAdapter, toOpenAIContent, fromOpenAIContent, } from './openai.js';
9
7
  export { OpenAICompatibleAdapter, toOpenAIMessages, fromOpenAIMessage, } from './openai-compatible.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GAErB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,oBAAoB,GAErB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,yBAAyB,GAE1B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,wBAAwB,GAEzB,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GAErB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,wBAAwB,GAEzB,MAAM,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/membrane",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "LLM middleware - a selective boundary that transforms what passes through",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,16 +9,6 @@ export {
9
9
  type AnthropicAdapterConfig,
10
10
  } from './anthropic.js';
11
11
 
12
- export {
13
- AnthropicChatAdapter,
14
- type AnthropicChatAdapterConfig,
15
- } from './anthropic-chat.js';
16
-
17
- export {
18
- AnthropicMultiuserAdapter,
19
- type AnthropicMultiuserAdapterConfig,
20
- } from './anthropic-multiuser.js';
21
-
22
12
  export {
23
13
  OpenRouterAdapter,
24
14
  toOpenRouterMessages,
@@ -1,294 +0,0 @@
1
- /**
2
- * Anthropic Chat Adapter - Simple two-party conversation
3
- *
4
- * For standard Human/Assistant conversations without multi-user support.
5
- * - Strict participant validation (only configured human/assistant names allowed)
6
- * - No participant names in messages (pure user/assistant roles)
7
- * - Native Anthropic tool API
8
- *
9
- * Use AnthropicMultiuserAdapter for multi-party conversations.
10
- */
11
-
12
- import Anthropic from '@anthropic-ai/sdk';
13
- import type {
14
- ProviderAdapter,
15
- ProviderRequest,
16
- ProviderRequestOptions,
17
- ProviderResponse,
18
- StreamCallbacks,
19
- ContentBlock,
20
- } from '../types/index.js';
21
- import {
22
- MembraneError,
23
- rateLimitError,
24
- contextLengthError,
25
- authError,
26
- serverError,
27
- abortError,
28
- } from '../types/index.js';
29
- import { toAnthropicContent, fromAnthropicContent } from './anthropic.js';
30
-
31
- // ============================================================================
32
- // Adapter Configuration
33
- // ============================================================================
34
-
35
- export interface AnthropicChatAdapterConfig {
36
- /** API key (defaults to ANTHROPIC_API_KEY env var) */
37
- apiKey?: string;
38
-
39
- /** Base URL override */
40
- baseURL?: string;
41
-
42
- /** Default max tokens */
43
- defaultMaxTokens?: number;
44
-
45
- /**
46
- * Human participant name (default: 'Human')
47
- * Messages with this participant become 'user' role.
48
- */
49
- humanParticipant?: string;
50
-
51
- /**
52
- * Assistant participant name (default: 'Claude')
53
- * Messages with this participant become 'assistant' role.
54
- */
55
- assistantParticipant?: string;
56
- }
57
-
58
- // ============================================================================
59
- // Anthropic Chat Adapter
60
- // ============================================================================
61
-
62
- export class AnthropicChatAdapter implements ProviderAdapter {
63
- readonly name = 'anthropic-chat';
64
- private client: Anthropic;
65
- private defaultMaxTokens: number;
66
- private humanParticipant: string;
67
- private assistantParticipant: string;
68
-
69
- constructor(config: AnthropicChatAdapterConfig = {}) {
70
- this.client = new Anthropic({
71
- apiKey: config.apiKey,
72
- baseURL: config.baseURL,
73
- });
74
- this.defaultMaxTokens = config.defaultMaxTokens ?? 4096;
75
- this.humanParticipant = config.humanParticipant ?? 'Human';
76
- this.assistantParticipant = config.assistantParticipant ?? 'Claude';
77
- }
78
-
79
- supportsModel(modelId: string): boolean {
80
- return modelId.startsWith('claude-');
81
- }
82
-
83
- async complete(
84
- request: ProviderRequest,
85
- options?: ProviderRequestOptions
86
- ): Promise<ProviderResponse> {
87
- const anthropicRequest = this.buildRequest(request);
88
- const fullRequest = { ...anthropicRequest, stream: false as const };
89
- options?.onRequest?.(fullRequest);
90
-
91
- try {
92
- const response = await this.client.messages.create(fullRequest, {
93
- signal: options?.signal,
94
- });
95
-
96
- return this.parseResponse(response, fullRequest);
97
- } catch (error) {
98
- throw this.handleError(error, fullRequest);
99
- }
100
- }
101
-
102
- async stream(
103
- request: ProviderRequest,
104
- callbacks: StreamCallbacks,
105
- options?: ProviderRequestOptions
106
- ): Promise<ProviderResponse> {
107
- const anthropicRequest = this.buildRequest(request);
108
- const fullRequest = { ...anthropicRequest, stream: true };
109
- options?.onRequest?.(fullRequest);
110
-
111
- try {
112
- const stream = await this.client.messages.stream(anthropicRequest, {
113
- signal: options?.signal,
114
- });
115
-
116
- let accumulated = '';
117
- const contentBlocks: unknown[] = [];
118
- let currentBlockIndex = -1;
119
-
120
- for await (const event of stream) {
121
- if (event.type === 'content_block_start') {
122
- currentBlockIndex = event.index;
123
- contentBlocks[currentBlockIndex] = event.content_block;
124
- callbacks.onContentBlock?.(currentBlockIndex, event.content_block);
125
- } else if (event.type === 'content_block_delta') {
126
- if (event.delta.type === 'text_delta') {
127
- const chunk = event.delta.text;
128
- accumulated += chunk;
129
- callbacks.onChunk(chunk);
130
- } else if (event.delta.type === 'thinking_delta') {
131
- callbacks.onChunk(event.delta.thinking);
132
- }
133
- } else if (event.type === 'content_block_stop') {
134
- callbacks.onContentBlock?.(currentBlockIndex, contentBlocks[currentBlockIndex]);
135
- }
136
- }
137
-
138
- const finalMessage = await stream.finalMessage();
139
- return this.parseResponse(finalMessage, fullRequest);
140
-
141
- } catch (error) {
142
- throw this.handleError(error, fullRequest);
143
- }
144
- }
145
-
146
- // ============================================================================
147
- // Message Conversion
148
- // ============================================================================
149
-
150
- /**
151
- * Convert normalized messages to Anthropic format.
152
- * Validates that only configured human/assistant participants are used.
153
- */
154
- private convertMessages(
155
- messages: Array<{ participant: string; content: ContentBlock[] }>
156
- ): Anthropic.MessageParam[] {
157
- const result: Anthropic.MessageParam[] = [];
158
-
159
- for (const msg of messages) {
160
- // Validate participant
161
- if (msg.participant !== this.humanParticipant && msg.participant !== this.assistantParticipant) {
162
- throw new MembraneError({
163
- type: 'invalid_request',
164
- message: `AnthropicChatAdapter only supports two participants: "${this.humanParticipant}" and "${this.assistantParticipant}". ` +
165
- `Got: "${msg.participant}". Use AnthropicMultiuserAdapter for multi-party conversations.`,
166
- retryable: false,
167
- rawError: new Error(`Invalid participant: ${msg.participant}`),
168
- });
169
- }
170
-
171
- const role: 'user' | 'assistant' = msg.participant === this.humanParticipant ? 'user' : 'assistant';
172
- const content = toAnthropicContent(msg.content);
173
-
174
- result.push({ role, content });
175
- }
176
-
177
- return result;
178
- }
179
-
180
- private buildRequest(request: ProviderRequest): Anthropic.MessageCreateParams {
181
- // Get normalized messages from extra (preferred) or fall back to provider messages
182
- const normalizedMessages = request.extra?.normalizedMessages as Array<{ participant: string; content: ContentBlock[] }> | undefined;
183
-
184
- let messages: Anthropic.MessageParam[];
185
- if (normalizedMessages) {
186
- messages = this.convertMessages(normalizedMessages);
187
- } else {
188
- // Assume already in provider format
189
- messages = request.messages as Anthropic.MessageParam[];
190
- }
191
-
192
- const params: Anthropic.MessageCreateParams = {
193
- model: request.model,
194
- max_tokens: request.maxTokens || this.defaultMaxTokens,
195
- messages,
196
- };
197
-
198
- // Handle system prompt
199
- if (request.system) {
200
- if (typeof request.system === 'string') {
201
- params.system = request.system;
202
- } else if (Array.isArray(request.system)) {
203
- params.system = request.system as Anthropic.TextBlockParam[];
204
- }
205
- }
206
-
207
- if (request.temperature !== undefined) {
208
- params.temperature = request.temperature;
209
- }
210
-
211
- if (request.stopSequences && request.stopSequences.length > 0) {
212
- params.stop_sequences = request.stopSequences;
213
- }
214
-
215
- if (request.tools && request.tools.length > 0) {
216
- params.tools = request.tools as Anthropic.Tool[];
217
- }
218
-
219
- // Handle extended thinking
220
- if ((request as any).thinking) {
221
- (params as any).thinking = (request as any).thinking;
222
- }
223
-
224
- // Apply extra params (excluding normalizedMessages)
225
- if (request.extra) {
226
- const { normalizedMessages: _, ...rest } = request.extra;
227
- Object.assign(params, rest);
228
- }
229
-
230
- return params;
231
- }
232
-
233
- private parseResponse(response: Anthropic.Message, rawRequest: unknown): ProviderResponse {
234
- return {
235
- content: fromAnthropicContent(response.content),
236
- stopReason: response.stop_reason ?? 'end_turn',
237
- stopSequence: response.stop_sequence ?? undefined,
238
- usage: {
239
- inputTokens: response.usage.input_tokens,
240
- outputTokens: response.usage.output_tokens,
241
- cacheCreationTokens: (response.usage as any).cache_creation_input_tokens,
242
- cacheReadTokens: (response.usage as any).cache_read_input_tokens,
243
- },
244
- model: response.model,
245
- rawRequest,
246
- raw: response,
247
- };
248
- }
249
-
250
- private handleError(error: unknown, rawRequest?: unknown): MembraneError {
251
- if (error instanceof Anthropic.APIError) {
252
- const status = error.status;
253
- const message = error.message;
254
-
255
- if (status === 429) {
256
- const retryAfter = this.parseRetryAfter(error);
257
- return rateLimitError(message, retryAfter, error, rawRequest);
258
- }
259
-
260
- if (status === 401) {
261
- return authError(message, error, rawRequest);
262
- }
263
-
264
- if (message.includes('context') || message.includes('too long')) {
265
- return contextLengthError(message, error, rawRequest);
266
- }
267
-
268
- if (status >= 500) {
269
- return serverError(message, status, error, rawRequest);
270
- }
271
- }
272
-
273
- if (error instanceof Error && error.name === 'AbortError') {
274
- return abortError(undefined, rawRequest);
275
- }
276
-
277
- return new MembraneError({
278
- type: 'unknown',
279
- message: error instanceof Error ? error.message : String(error),
280
- retryable: false,
281
- rawError: error,
282
- rawRequest,
283
- });
284
- }
285
-
286
- private parseRetryAfter(error: { message: string }): number | undefined {
287
- const message = error.message;
288
- const match = message.match(/retry after (\d+)/i);
289
- if (match && match[1]) {
290
- return parseInt(match[1], 10) * 1000;
291
- }
292
- return undefined;
293
- }
294
- }
@@ -1,387 +0,0 @@
1
- /**
2
- * Anthropic Multiuser Adapter - Multi-party conversation support
3
- *
4
- * For conversations with multiple participants (e.g., group chats, Discord).
5
- * - All non-bot participants map to 'user' role
6
- * - Bot participant maps to 'assistant' role
7
- * - Prefixes messages with participant names for context
8
- * - Native Anthropic tool API
9
- *
10
- * Use AnthropicChatAdapter for simple two-party Human/Assistant conversations.
11
- */
12
-
13
- import Anthropic from '@anthropic-ai/sdk';
14
- import type {
15
- ProviderAdapter,
16
- ProviderRequest,
17
- ProviderRequestOptions,
18
- ProviderResponse,
19
- StreamCallbacks,
20
- ContentBlock,
21
- } from '../types/index.js';
22
- import {
23
- MembraneError,
24
- rateLimitError,
25
- contextLengthError,
26
- authError,
27
- serverError,
28
- abortError,
29
- } from '../types/index.js';
30
- import { fromAnthropicContent } from './anthropic.js';
31
-
32
- // ============================================================================
33
- // Adapter Configuration
34
- // ============================================================================
35
-
36
- export interface AnthropicMultiuserAdapterConfig {
37
- /** API key (defaults to ANTHROPIC_API_KEY env var) */
38
- apiKey?: string;
39
-
40
- /** Base URL override */
41
- baseURL?: string;
42
-
43
- /** Default max tokens */
44
- defaultMaxTokens?: number;
45
-
46
- /**
47
- * Bot/assistant participant name (default: 'Claude')
48
- * Messages with this participant become 'assistant' role (no name prefix).
49
- */
50
- assistantParticipant?: string;
51
-
52
- /**
53
- * Whether to prefix user messages with participant names (default: true)
54
- * When true: "Alice: Hello there"
55
- * When false: "Hello there"
56
- */
57
- includeParticipantNames?: boolean;
58
-
59
- /**
60
- * Format for participant name prefix (default: '{name}: ')
61
- * Use {name} as placeholder for participant name.
62
- */
63
- nameFormat?: string;
64
- }
65
-
66
- // ============================================================================
67
- // Anthropic Multiuser Adapter
68
- // ============================================================================
69
-
70
- export class AnthropicMultiuserAdapter implements ProviderAdapter {
71
- readonly name = 'anthropic-multiuser';
72
- private client: Anthropic;
73
- private defaultMaxTokens: number;
74
- private assistantParticipant: string;
75
- private includeParticipantNames: boolean;
76
- private nameFormat: string;
77
-
78
- constructor(config: AnthropicMultiuserAdapterConfig = {}) {
79
- this.client = new Anthropic({
80
- apiKey: config.apiKey,
81
- baseURL: config.baseURL,
82
- });
83
- this.defaultMaxTokens = config.defaultMaxTokens ?? 4096;
84
- this.assistantParticipant = config.assistantParticipant ?? 'Claude';
85
- this.includeParticipantNames = config.includeParticipantNames ?? true;
86
- this.nameFormat = config.nameFormat ?? '{name}: ';
87
- }
88
-
89
- supportsModel(modelId: string): boolean {
90
- return modelId.startsWith('claude-');
91
- }
92
-
93
- async complete(
94
- request: ProviderRequest,
95
- options?: ProviderRequestOptions
96
- ): Promise<ProviderResponse> {
97
- const anthropicRequest = this.buildRequest(request);
98
- const fullRequest = { ...anthropicRequest, stream: false as const };
99
- options?.onRequest?.(fullRequest);
100
-
101
- try {
102
- const response = await this.client.messages.create(fullRequest, {
103
- signal: options?.signal,
104
- });
105
-
106
- return this.parseResponse(response, fullRequest);
107
- } catch (error) {
108
- throw this.handleError(error, fullRequest);
109
- }
110
- }
111
-
112
- async stream(
113
- request: ProviderRequest,
114
- callbacks: StreamCallbacks,
115
- options?: ProviderRequestOptions
116
- ): Promise<ProviderResponse> {
117
- const anthropicRequest = this.buildRequest(request);
118
- const fullRequest = { ...anthropicRequest, stream: true };
119
- options?.onRequest?.(fullRequest);
120
-
121
- try {
122
- const stream = await this.client.messages.stream(anthropicRequest, {
123
- signal: options?.signal,
124
- });
125
-
126
- let accumulated = '';
127
- const contentBlocks: unknown[] = [];
128
- let currentBlockIndex = -1;
129
-
130
- for await (const event of stream) {
131
- if (event.type === 'content_block_start') {
132
- currentBlockIndex = event.index;
133
- contentBlocks[currentBlockIndex] = event.content_block;
134
- callbacks.onContentBlock?.(currentBlockIndex, event.content_block);
135
- } else if (event.type === 'content_block_delta') {
136
- if (event.delta.type === 'text_delta') {
137
- const chunk = event.delta.text;
138
- accumulated += chunk;
139
- callbacks.onChunk(chunk);
140
- } else if (event.delta.type === 'thinking_delta') {
141
- callbacks.onChunk(event.delta.thinking);
142
- }
143
- } else if (event.type === 'content_block_stop') {
144
- callbacks.onContentBlock?.(currentBlockIndex, contentBlocks[currentBlockIndex]);
145
- }
146
- }
147
-
148
- const finalMessage = await stream.finalMessage();
149
- return this.parseResponse(finalMessage, fullRequest);
150
-
151
- } catch (error) {
152
- throw this.handleError(error, fullRequest);
153
- }
154
- }
155
-
156
- // ============================================================================
157
- // Message Conversion
158
- // ============================================================================
159
-
160
- /**
161
- * Convert normalized messages to Anthropic format.
162
- * - Bot messages become assistant role
163
- * - All other messages become user role with optional name prefix
164
- */
165
- private convertMessages(
166
- messages: Array<{ participant: string; content: ContentBlock[] }>
167
- ): Anthropic.MessageParam[] {
168
- const result: Anthropic.MessageParam[] = [];
169
-
170
- for (const msg of messages) {
171
- const isAssistant = msg.participant === this.assistantParticipant;
172
- const role: 'user' | 'assistant' = isAssistant ? 'assistant' : 'user';
173
-
174
- // Convert content blocks
175
- const content: Anthropic.ContentBlockParam[] = [];
176
-
177
- for (const block of msg.content) {
178
- if (block.type === 'text') {
179
- let text = block.text;
180
-
181
- // Prefix with participant name for non-assistant messages
182
- if (!isAssistant && this.includeParticipantNames) {
183
- const prefix = this.nameFormat.replace('{name}', msg.participant);
184
- text = prefix + text;
185
- }
186
-
187
- const textBlock: any = { type: 'text', text };
188
- if (block.cache_control) {
189
- textBlock.cache_control = block.cache_control;
190
- }
191
- content.push(textBlock);
192
- } else if (block.type === 'image' && block.source.type === 'base64') {
193
- content.push({
194
- type: 'image',
195
- source: {
196
- type: 'base64',
197
- media_type: block.source.mediaType as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp',
198
- data: block.source.data,
199
- },
200
- });
201
- } else if (block.type === 'document') {
202
- content.push({
203
- type: 'document',
204
- source: {
205
- type: 'base64',
206
- media_type: block.source.mediaType as 'application/pdf',
207
- data: block.source.data,
208
- },
209
- });
210
- } else if (block.type === 'tool_use') {
211
- content.push({
212
- type: 'tool_use',
213
- id: block.id,
214
- name: block.name,
215
- input: block.input,
216
- });
217
- } else if (block.type === 'tool_result') {
218
- content.push({
219
- type: 'tool_result',
220
- tool_use_id: block.toolUseId,
221
- content: typeof block.content === 'string'
222
- ? block.content
223
- : JSON.stringify(block.content),
224
- is_error: block.isError,
225
- });
226
- } else if (block.type === 'thinking') {
227
- content.push({
228
- type: 'thinking',
229
- thinking: block.thinking,
230
- } as any);
231
- }
232
- }
233
-
234
- result.push({ role, content });
235
- }
236
-
237
- // Anthropic requires alternating user/assistant messages
238
- // Merge consecutive same-role messages
239
- return this.mergeConsecutiveRoles(result);
240
- }
241
-
242
- /**
243
- * Merge consecutive messages with the same role.
244
- * Anthropic API requires strictly alternating user/assistant messages.
245
- */
246
- private mergeConsecutiveRoles(messages: Anthropic.MessageParam[]): Anthropic.MessageParam[] {
247
- if (messages.length === 0) return [];
248
-
249
- const merged: Anthropic.MessageParam[] = [];
250
- let current: Anthropic.MessageParam = messages[0]!;
251
-
252
- for (let i = 1; i < messages.length; i++) {
253
- const next: Anthropic.MessageParam = messages[i]!;
254
-
255
- if (next.role === current.role) {
256
- // Merge content arrays
257
- const currentContent = Array.isArray(current.content) ? current.content : [{ type: 'text' as const, text: current.content }];
258
- const nextContent = Array.isArray(next.content) ? next.content : [{ type: 'text' as const, text: next.content }];
259
- current = {
260
- role: current.role,
261
- content: [...currentContent, ...nextContent],
262
- };
263
- } else {
264
- merged.push(current);
265
- current = next;
266
- }
267
- }
268
-
269
- merged.push(current);
270
- return merged;
271
- }
272
-
273
- private buildRequest(request: ProviderRequest): Anthropic.MessageCreateParams {
274
- // Get normalized messages from extra (preferred) or fall back to provider messages
275
- const normalizedMessages = request.extra?.normalizedMessages as Array<{ participant: string; content: ContentBlock[] }> | undefined;
276
-
277
- let messages: Anthropic.MessageParam[];
278
- if (normalizedMessages) {
279
- messages = this.convertMessages(normalizedMessages);
280
- } else {
281
- // Assume already in provider format
282
- messages = request.messages as Anthropic.MessageParam[];
283
- }
284
-
285
- const params: Anthropic.MessageCreateParams = {
286
- model: request.model,
287
- max_tokens: request.maxTokens || this.defaultMaxTokens,
288
- messages,
289
- };
290
-
291
- // Handle system prompt
292
- if (request.system) {
293
- if (typeof request.system === 'string') {
294
- params.system = request.system;
295
- } else if (Array.isArray(request.system)) {
296
- params.system = request.system as Anthropic.TextBlockParam[];
297
- }
298
- }
299
-
300
- if (request.temperature !== undefined) {
301
- params.temperature = request.temperature;
302
- }
303
-
304
- if (request.stopSequences && request.stopSequences.length > 0) {
305
- params.stop_sequences = request.stopSequences;
306
- }
307
-
308
- if (request.tools && request.tools.length > 0) {
309
- params.tools = request.tools as Anthropic.Tool[];
310
- }
311
-
312
- // Handle extended thinking
313
- if ((request as any).thinking) {
314
- (params as any).thinking = (request as any).thinking;
315
- }
316
-
317
- // Apply extra params (excluding normalizedMessages)
318
- if (request.extra) {
319
- const { normalizedMessages: _, ...rest } = request.extra;
320
- Object.assign(params, rest);
321
- }
322
-
323
- return params;
324
- }
325
-
326
- private parseResponse(response: Anthropic.Message, rawRequest: unknown): ProviderResponse {
327
- return {
328
- content: fromAnthropicContent(response.content),
329
- stopReason: response.stop_reason ?? 'end_turn',
330
- stopSequence: response.stop_sequence ?? undefined,
331
- usage: {
332
- inputTokens: response.usage.input_tokens,
333
- outputTokens: response.usage.output_tokens,
334
- cacheCreationTokens: (response.usage as any).cache_creation_input_tokens,
335
- cacheReadTokens: (response.usage as any).cache_read_input_tokens,
336
- },
337
- model: response.model,
338
- rawRequest,
339
- raw: response,
340
- };
341
- }
342
-
343
- private handleError(error: unknown, rawRequest?: unknown): MembraneError {
344
- if (error instanceof Anthropic.APIError) {
345
- const status = error.status;
346
- const message = error.message;
347
-
348
- if (status === 429) {
349
- const retryAfter = this.parseRetryAfter(error);
350
- return rateLimitError(message, retryAfter, error, rawRequest);
351
- }
352
-
353
- if (status === 401) {
354
- return authError(message, error, rawRequest);
355
- }
356
-
357
- if (message.includes('context') || message.includes('too long')) {
358
- return contextLengthError(message, error, rawRequest);
359
- }
360
-
361
- if (status >= 500) {
362
- return serverError(message, status, error, rawRequest);
363
- }
364
- }
365
-
366
- if (error instanceof Error && error.name === 'AbortError') {
367
- return abortError(undefined, rawRequest);
368
- }
369
-
370
- return new MembraneError({
371
- type: 'unknown',
372
- message: error instanceof Error ? error.message : String(error),
373
- retryable: false,
374
- rawError: error,
375
- rawRequest,
376
- });
377
- }
378
-
379
- private parseRetryAfter(error: { message: string }): number | undefined {
380
- const message = error.message;
381
- const match = message.match(/retry after (\d+)/i);
382
- if (match && match[1]) {
383
- return parseInt(match[1], 10) * 1000;
384
- }
385
- return undefined;
386
- }
387
- }