@animalabs/membrane 0.5.40 → 0.5.42

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 (49) hide show
  1. package/dist/formatters/pseudo-prefill.d.ts +71 -0
  2. package/dist/formatters/pseudo-prefill.d.ts.map +1 -0
  3. package/dist/formatters/pseudo-prefill.js +410 -0
  4. package/dist/formatters/pseudo-prefill.js.map +1 -0
  5. package/dist/membrane.d.ts +5 -0
  6. package/dist/membrane.d.ts.map +1 -1
  7. package/dist/membrane.js +91 -44
  8. package/dist/membrane.js.map +1 -1
  9. package/dist/providers/bedrock.d.ts.map +1 -1
  10. package/dist/providers/bedrock.js +27 -8
  11. package/dist/providers/bedrock.js.map +1 -1
  12. package/dist/providers/gemini.d.ts.map +1 -1
  13. package/dist/providers/gemini.js +11 -2
  14. package/dist/providers/gemini.js.map +1 -1
  15. package/dist/providers/openai-compatible.d.ts.map +1 -1
  16. package/dist/providers/openai-compatible.js +27 -16
  17. package/dist/providers/openai-compatible.js.map +1 -1
  18. package/dist/providers/openai-completions.d.ts.map +1 -1
  19. package/dist/providers/openai-completions.js +48 -24
  20. package/dist/providers/openai-completions.js.map +1 -1
  21. package/dist/providers/openai-responses.d.ts.map +1 -1
  22. package/dist/providers/openai-responses.js +6 -1
  23. package/dist/providers/openai-responses.js.map +1 -1
  24. package/dist/providers/openai.d.ts.map +1 -1
  25. package/dist/providers/openai.js +27 -16
  26. package/dist/providers/openai.js.map +1 -1
  27. package/dist/providers/openrouter.d.ts.map +1 -1
  28. package/dist/providers/openrouter.js +37 -17
  29. package/dist/providers/openrouter.js.map +1 -1
  30. package/dist/providers/utils.d.ts +44 -0
  31. package/dist/providers/utils.d.ts.map +1 -0
  32. package/dist/providers/utils.js +100 -0
  33. package/dist/providers/utils.js.map +1 -0
  34. package/dist/transforms/prefill.d.ts.map +1 -1
  35. package/dist/transforms/prefill.js +44 -45
  36. package/dist/transforms/prefill.js.map +1 -1
  37. package/dist/types/request.d.ts +8 -0
  38. package/dist/types/request.d.ts.map +1 -1
  39. package/package.json +2 -1
  40. package/src/membrane.ts +96 -46
  41. package/src/providers/bedrock.ts +25 -9
  42. package/src/providers/gemini.ts +9 -2
  43. package/src/providers/openai-compatible.ts +27 -18
  44. package/src/providers/openai-completions.ts +47 -25
  45. package/src/providers/openai-responses.ts +5 -1
  46. package/src/providers/openai.ts +27 -18
  47. package/src/providers/openrouter.ts +36 -19
  48. package/src/providers/utils.ts +109 -0
  49. package/src/types/request.ts +9 -0
@@ -20,6 +20,7 @@ import {
20
20
  serverError,
21
21
  abortError,
22
22
  } from '../types/index.js';
23
+ import { createCombinedSignal } from './utils.js';
23
24
 
24
25
  // ============================================================================
25
26
  // Adapter Configuration
@@ -274,8 +275,8 @@ export class BedrockAdapter implements ProviderAdapter {
274
275
 
275
276
  // If already in Bedrock format, use as-is
276
277
  // Supports both direct model IDs (anthropic.claude-...) and
277
- // cross-region inference profile IDs (us.anthropic.claude-..., eu.anthropic.claude-...)
278
- if (modelId.startsWith('anthropic.') || /^[a-z]{2}\.anthropic\./.test(modelId)) {
278
+ // cross-region inference profile IDs (us.anthropic.claude-..., eu.anthropic.claude-..., apac.anthropic.claude-...)
279
+ if (modelId.startsWith('anthropic.') || /^[a-z]{2,4}\.anthropic\./.test(modelId)) {
279
280
  return modelId;
280
281
  }
281
282
 
@@ -306,11 +307,14 @@ export class BedrockAdapter implements ProviderAdapter {
306
307
  const fullRequest = { modelId: bedrockModelId, ...bedrockRequest };
307
308
  options?.onRequest?.(fullRequest);
308
309
 
310
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
309
311
  try {
310
- const response = await this.invokeModel(bedrockModelId, bedrockRequest, options?.signal);
312
+ const response = await this.invokeModel(bedrockModelId, bedrockRequest, combinedSignal);
311
313
  return this.parseResponse(response, fullRequest);
312
314
  } catch (error) {
313
315
  throw this.handleError(error, fullRequest);
316
+ } finally {
317
+ cleanup?.();
314
318
  }
315
319
  }
316
320
 
@@ -324,10 +328,13 @@ export class BedrockAdapter implements ProviderAdapter {
324
328
  const fullRequest = { modelId: bedrockModelId, ...bedrockRequest, stream: true };
325
329
  options?.onRequest?.(fullRequest);
326
330
 
331
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
327
332
  try {
328
- return await this.invokeModelWithStream(bedrockModelId, bedrockRequest, callbacks, options?.signal);
333
+ return await this.invokeModelWithStream(bedrockModelId, bedrockRequest, callbacks, combinedSignal);
329
334
  } catch (error) {
330
335
  throw this.handleError(error, fullRequest);
336
+ } finally {
337
+ cleanup?.();
331
338
  }
332
339
  }
333
340
 
@@ -477,7 +484,7 @@ export class BedrockAdapter implements ProviderAdapter {
477
484
  }
478
485
 
479
486
  // Parse the binary event stream
480
- const contentBlocks: Array<{ type: string; text?: string }> = [];
487
+ const contentBlocks: Array<{ type: string; text?: string; thinking?: string; signature?: string }> = [];
481
488
  let currentBlockIndex = -1;
482
489
  let finalMessage: BedrockMessageResponse | undefined;
483
490
  let inputTokens = 0;
@@ -583,6 +590,13 @@ export class BedrockAdapter implements ProviderAdapter {
583
590
  }
584
591
  } else if (eventData.delta?.type === 'thinking_delta' && eventData.delta.thinking) {
585
592
  callbacks.onChunk(eventData.delta.thinking);
593
+ if (contentBlocks[currentBlockIndex]) {
594
+ contentBlocks[currentBlockIndex]!.thinking = (contentBlocks[currentBlockIndex]!.thinking ?? '') + eventData.delta.thinking;
595
+ }
596
+ } else if (eventData.delta?.type === 'signature_delta' && (eventData.delta as any).signature) {
597
+ if (contentBlocks[currentBlockIndex]) {
598
+ contentBlocks[currentBlockIndex]!.signature = (contentBlocks[currentBlockIndex]!.signature ?? '') + (eventData.delta as any).signature;
599
+ }
586
600
  }
587
601
  } else if (eventData.type === 'message_delta') {
588
602
  if (eventData.usage) {
@@ -611,10 +625,12 @@ export class BedrockAdapter implements ProviderAdapter {
611
625
  id: 'msg_stream',
612
626
  type: 'message',
613
627
  role: 'assistant',
614
- content: contentBlocks.map(b => ({
615
- type: b.type as 'text',
616
- text: b.text,
617
- })),
628
+ content: contentBlocks.map(b => {
629
+ if (b.type === 'thinking') {
630
+ return { type: 'thinking' as const, thinking: b.thinking, signature: b.signature };
631
+ }
632
+ return { type: b.type as 'text', text: b.text };
633
+ }),
618
634
  model: modelId,
619
635
  stop_reason: stopReason as BedrockMessageResponse['stop_reason'],
620
636
  usage: {
@@ -28,6 +28,7 @@ import {
28
28
  abortError,
29
29
  networkError,
30
30
  } from '../types/index.js';
31
+ import { createCombinedSignal } from './utils.js';
31
32
 
32
33
  // ============================================================================
33
34
  // Gemini API Types
@@ -127,13 +128,14 @@ export class GeminiAdapter implements ProviderAdapter {
127
128
  const geminiRequest = this.buildRequest(request);
128
129
  options?.onRequest?.(geminiRequest);
129
130
 
131
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
130
132
  try {
131
133
  const url = `${this.baseURL}/models/${request.model}:generateContent?key=${this.apiKey}`;
132
134
  const response = await fetch(url, {
133
135
  method: 'POST',
134
136
  headers: { 'Content-Type': 'application/json' },
135
137
  body: JSON.stringify(geminiRequest),
136
- signal: options?.signal,
138
+ signal: combinedSignal,
137
139
  });
138
140
 
139
141
  if (!response.ok) {
@@ -150,6 +152,8 @@ export class GeminiAdapter implements ProviderAdapter {
150
152
  return this.parseResponse(data, request.model, geminiRequest);
151
153
  } catch (error) {
152
154
  throw this.handleError(error, geminiRequest);
155
+ } finally {
156
+ cleanup?.();
153
157
  }
154
158
  }
155
159
 
@@ -161,13 +165,14 @@ export class GeminiAdapter implements ProviderAdapter {
161
165
  const geminiRequest = this.buildRequest(request);
162
166
  options?.onRequest?.(geminiRequest);
163
167
 
168
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
164
169
  try {
165
170
  const url = `${this.baseURL}/models/${request.model}:streamGenerateContent?alt=sse&key=${this.apiKey}`;
166
171
  const response = await fetch(url, {
167
172
  method: 'POST',
168
173
  headers: { 'Content-Type': 'application/json' },
169
174
  body: JSON.stringify(geminiRequest),
170
- signal: options?.signal,
175
+ signal: combinedSignal,
171
176
  });
172
177
 
173
178
  if (!response.ok) {
@@ -300,6 +305,8 @@ export class GeminiAdapter implements ProviderAdapter {
300
305
  };
301
306
  } catch (error) {
302
307
  throw this.handleError(error, geminiRequest);
308
+ } finally {
309
+ cleanup?.();
303
310
  }
304
311
  }
305
312
 
@@ -30,6 +30,7 @@ import {
30
30
  abortError,
31
31
  networkError,
32
32
  } from '../types/index.js';
33
+ import { safeParseJson, createCombinedSignal, SSELineParser } from './utils.js';
33
34
 
34
35
  // ============================================================================
35
36
  // Types
@@ -155,12 +156,13 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
155
156
  openAIRequest.stream = true;
156
157
  options?.onRequest?.(openAIRequest);
157
158
 
159
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
158
160
  try {
159
161
  const response = await fetch(`${this.baseURL}/chat/completions`, {
160
162
  method: 'POST',
161
163
  headers: this.getHeaders(),
162
164
  body: JSON.stringify(openAIRequest),
163
- signal: options?.signal,
165
+ signal: combinedSignal,
164
166
  });
165
167
 
166
168
  if (!response.ok) {
@@ -174,6 +176,7 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
174
176
  }
175
177
 
176
178
  const decoder = new TextDecoder();
179
+ const sseParser = new SSELineParser();
177
180
  let accumulated = '';
178
181
  let finishReason = 'stop';
179
182
  let toolCalls: OpenAIToolCall[] = [];
@@ -183,10 +186,9 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
183
186
  if (done) break;
184
187
 
185
188
  const chunk = decoder.decode(value, { stream: true });
186
- const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
189
+ const dataLines = sseParser.feed(chunk);
187
190
 
188
- for (const line of lines) {
189
- const data = line.slice(6);
191
+ for (const data of dataLines) {
190
192
  if (data === '[DONE]') continue;
191
193
 
192
194
  try {
@@ -240,6 +242,8 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
240
242
 
241
243
  } catch (error) {
242
244
  throw this.handleError(error, openAIRequest);
245
+ } finally {
246
+ cleanup?.();
243
247
  }
244
248
  }
245
249
 
@@ -415,19 +419,24 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
415
419
  }
416
420
 
417
421
  private async makeRequest(request: any, options?: ProviderRequestOptions): Promise<OpenAIResponse> {
418
- const response = await fetch(`${this.baseURL}/chat/completions`, {
419
- method: 'POST',
420
- headers: this.getHeaders(),
421
- body: JSON.stringify(request),
422
- signal: options?.signal,
423
- });
424
-
425
- if (!response.ok) {
426
- const errorText = await response.text();
427
- throw new Error(`API error: ${response.status} ${errorText}`);
422
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
423
+ try {
424
+ const response = await fetch(`${this.baseURL}/chat/completions`, {
425
+ method: 'POST',
426
+ headers: this.getHeaders(),
427
+ body: JSON.stringify(request),
428
+ signal: combinedSignal,
429
+ });
430
+
431
+ if (!response.ok) {
432
+ const errorText = await response.text();
433
+ throw new Error(`API error: ${response.status} ${errorText}`);
434
+ }
435
+
436
+ return await response.json() as OpenAIResponse;
437
+ } finally {
438
+ cleanup?.();
428
439
  }
429
-
430
- return response.json() as Promise<OpenAIResponse>;
431
440
  }
432
441
 
433
442
  private parseResponse(response: OpenAIResponse, requestedModel: string, rawRequest: unknown): ProviderResponse {
@@ -484,7 +493,7 @@ export class OpenAICompatibleAdapter implements ProviderAdapter {
484
493
  type: 'tool_use',
485
494
  id: tc.id,
486
495
  name: tc.function.name,
487
- input: JSON.parse(tc.function.arguments || '{}'),
496
+ input: safeParseJson(tc.function.arguments),
488
497
  });
489
498
  }
490
499
  }
@@ -625,7 +634,7 @@ export function fromOpenAIMessage(message: OpenAIMessage): ContentBlock[] {
625
634
  type: 'tool_use',
626
635
  id: tc.id,
627
636
  name: tc.function.name,
628
- input: JSON.parse(tc.function.arguments || '{}'),
637
+ input: safeParseJson(tc.function.arguments),
629
638
  });
630
639
  }
631
640
  }
@@ -27,6 +27,7 @@ import {
27
27
  abortError,
28
28
  networkError,
29
29
  } from '../types/index.js';
30
+ import { createCombinedSignal, SSELineParser } from './utils.js';
30
31
 
31
32
  // ============================================================================
32
33
  // Types
@@ -169,12 +170,13 @@ export class OpenAICompletionsAdapter implements ProviderAdapter {
169
170
  completionsRequest.stream = true;
170
171
  options?.onRequest?.(completionsRequest);
171
172
 
173
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
172
174
  try {
173
175
  const response = await fetch(`${this.baseURL}/completions`, {
174
176
  method: 'POST',
175
177
  headers: this.getHeaders(),
176
178
  body: JSON.stringify(completionsRequest),
177
- signal: options?.signal,
179
+ signal: combinedSignal,
178
180
  });
179
181
 
180
182
  if (!response.ok) {
@@ -188,6 +190,7 @@ export class OpenAICompletionsAdapter implements ProviderAdapter {
188
190
  }
189
191
 
190
192
  const decoder = new TextDecoder();
193
+ const sseParser = new SSELineParser();
191
194
  let accumulated = '';
192
195
  let finishReason = 'stop';
193
196
 
@@ -196,10 +199,9 @@ export class OpenAICompletionsAdapter implements ProviderAdapter {
196
199
  if (done) break;
197
200
 
198
201
  const chunk = decoder.decode(value, { stream: true });
199
- const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
202
+ const dataLines = sseParser.feed(chunk);
200
203
 
201
- for (const line of lines) {
202
- const data = line.slice(6);
204
+ for (const data of dataLines) {
203
205
  if (data === '[DONE]') continue;
204
206
 
205
207
  try {
@@ -224,6 +226,8 @@ export class OpenAICompletionsAdapter implements ProviderAdapter {
224
226
 
225
227
  } catch (error) {
226
228
  throw this.handleError(error, completionsRequest);
229
+ } finally {
230
+ cleanup?.();
227
231
  }
228
232
  }
229
233
 
@@ -333,9 +337,29 @@ export class OpenAICompletionsAdapter implements ProviderAdapter {
333
337
  }
334
338
 
335
339
  private buildRequest(request: ProviderRequest): CompletionsRequest {
336
- // Prefer normalizedMessages (has participant names) over messages (has role)
337
- const messages = (request.extra?.normalizedMessages as any[]) || (request.messages as any[]);
338
- const { prompt, participants } = this.serializeToPrompt(messages);
340
+ let prompt: string;
341
+ let stopSequences: string[];
342
+
343
+ if (typeof request.extra?.prompt === 'string') {
344
+ // Continuation path: prompt is already serialized, skip re-serialization.
345
+ // No participant-based stops or eotToken — the prompt already contains them.
346
+ prompt = request.extra.prompt;
347
+ stopSequences = [
348
+ ...this.extraStopSequences,
349
+ ...(request.stopSequences || []),
350
+ ];
351
+ } else {
352
+ // Normal path: serialize messages into prompt format
353
+ const messages = (request.extra?.normalizedMessages as any[]) || (request.messages as any[]);
354
+ const result = this.serializeToPrompt(messages);
355
+ prompt = result.prompt;
356
+ stopSequences = [
357
+ ...this.generateStopSequences(result.participants),
358
+ ...(this.eotToken ? [this.eotToken] : []),
359
+ ...this.extraStopSequences,
360
+ ...(request.stopSequences || []),
361
+ ];
362
+ }
339
363
 
340
364
  const params: CompletionsRequest = {
341
365
  model: request.model,
@@ -359,13 +383,6 @@ export class OpenAICompletionsAdapter implements ProviderAdapter {
359
383
  params.frequency_penalty = request.frequencyPenalty;
360
384
  }
361
385
 
362
- // Generate stop sequences from participant names + EOT token + any extras
363
- const stopSequences = [
364
- ...this.generateStopSequences(participants),
365
- ...(this.eotToken ? [this.eotToken] : []),
366
- ...this.extraStopSequences,
367
- ...(request.stopSequences || []),
368
- ];
369
386
  if (stopSequences.length > 0) {
370
387
  params.stop = stopSequences;
371
388
  }
@@ -380,19 +397,24 @@ export class OpenAICompletionsAdapter implements ProviderAdapter {
380
397
  }
381
398
 
382
399
  private async makeRequest(request: CompletionsRequest, options?: ProviderRequestOptions): Promise<CompletionsResponse> {
383
- const response = await fetch(`${this.baseURL}/completions`, {
384
- method: 'POST',
385
- headers: this.getHeaders(),
386
- body: JSON.stringify(request),
387
- signal: options?.signal,
388
- });
400
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
401
+ try {
402
+ const response = await fetch(`${this.baseURL}/completions`, {
403
+ method: 'POST',
404
+ headers: this.getHeaders(),
405
+ body: JSON.stringify(request),
406
+ signal: combinedSignal,
407
+ });
389
408
 
390
- if (!response.ok) {
391
- const errorText = await response.text();
392
- throw new Error(`API error: ${response.status} ${errorText}`);
393
- }
409
+ if (!response.ok) {
410
+ const errorText = await response.text();
411
+ throw new Error(`API error: ${response.status} ${errorText}`);
412
+ }
394
413
 
395
- return response.json() as Promise<CompletionsResponse>;
414
+ return await response.json() as CompletionsResponse;
415
+ } finally {
416
+ cleanup?.();
417
+ }
396
418
  }
397
419
 
398
420
  private parseResponse(response: CompletionsResponse, requestedModel: string, rawRequest: unknown): ProviderResponse {
@@ -39,6 +39,7 @@ import {
39
39
  abortError,
40
40
  networkError,
41
41
  } from '../types/index.js';
42
+ import { createCombinedSignal } from './utils.js';
42
43
 
43
44
  // ============================================================================
44
45
  // Images API Types
@@ -152,10 +153,11 @@ export class OpenAIResponsesAdapter implements ProviderAdapter {
152
153
  const imagesRequest = this.buildRequest(request, inputImages);
153
154
  options?.onRequest?.(imagesRequest);
154
155
 
156
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
155
157
  try {
156
158
  const fetchOptions: RequestInit = {
157
159
  method: 'POST',
158
- signal: options?.signal,
160
+ signal: combinedSignal,
159
161
  };
160
162
 
161
163
  if (isEdit) {
@@ -198,6 +200,8 @@ export class OpenAIResponsesAdapter implements ProviderAdapter {
198
200
  return this.parseResponse(data, request.model, imagesRequest);
199
201
  } catch (error) {
200
202
  throw this.handleError(error, imagesRequest);
203
+ } finally {
204
+ cleanup?.();
201
205
  }
202
206
  }
203
207
 
@@ -29,6 +29,7 @@ import {
29
29
  abortError,
30
30
  networkError,
31
31
  } from '../types/index.js';
32
+ import { safeParseJson, createCombinedSignal, SSELineParser } from './utils.js';
32
33
 
33
34
  // ============================================================================
34
35
  // Types
@@ -239,12 +240,13 @@ export class OpenAIAdapter implements ProviderAdapter {
239
240
  openAIRequest.stream_options = { include_usage: true };
240
241
  options?.onRequest?.(openAIRequest);
241
242
 
243
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
242
244
  try {
243
245
  const response = await fetch(`${this.baseURL}/chat/completions`, {
244
246
  method: 'POST',
245
247
  headers: this.getHeaders(),
246
248
  body: JSON.stringify(openAIRequest),
247
- signal: options?.signal,
249
+ signal: combinedSignal,
248
250
  });
249
251
 
250
252
  if (!response.ok) {
@@ -258,6 +260,7 @@ export class OpenAIAdapter implements ProviderAdapter {
258
260
  }
259
261
 
260
262
  const decoder = new TextDecoder();
263
+ const sseParser = new SSELineParser();
261
264
  let accumulated = '';
262
265
  let finishReason = 'stop';
263
266
  let toolCalls: OpenAIToolCall[] = [];
@@ -268,10 +271,9 @@ export class OpenAIAdapter implements ProviderAdapter {
268
271
  if (done) break;
269
272
 
270
273
  const chunk = decoder.decode(value, { stream: true });
271
- const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
274
+ const dataLines = sseParser.feed(chunk);
272
275
 
273
- for (const line of lines) {
274
- const data = line.slice(6);
276
+ for (const data of dataLines) {
275
277
  if (data === '[DONE]') continue;
276
278
 
277
279
  try {
@@ -330,6 +332,8 @@ export class OpenAIAdapter implements ProviderAdapter {
330
332
 
331
333
  } catch (error) {
332
334
  throw this.handleError(error, openAIRequest);
335
+ } finally {
336
+ cleanup?.();
333
337
  }
334
338
  }
335
339
 
@@ -513,19 +517,24 @@ export class OpenAIAdapter implements ProviderAdapter {
513
517
  }
514
518
 
515
519
  private async makeRequest(request: any, options?: ProviderRequestOptions): Promise<OpenAIResponse> {
516
- const response = await fetch(`${this.baseURL}/chat/completions`, {
517
- method: 'POST',
518
- headers: this.getHeaders(),
519
- body: JSON.stringify(request),
520
- signal: options?.signal,
521
- });
522
-
523
- if (!response.ok) {
524
- const errorText = await response.text();
525
- throw new Error(`OpenAI API error: ${response.status} ${errorText}`);
520
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
521
+ try {
522
+ const response = await fetch(`${this.baseURL}/chat/completions`, {
523
+ method: 'POST',
524
+ headers: this.getHeaders(),
525
+ body: JSON.stringify(request),
526
+ signal: combinedSignal,
527
+ });
528
+
529
+ if (!response.ok) {
530
+ const errorText = await response.text();
531
+ throw new Error(`OpenAI API error: ${response.status} ${errorText}`);
532
+ }
533
+
534
+ return await response.json() as OpenAIResponse;
535
+ } finally {
536
+ cleanup?.();
526
537
  }
527
-
528
- return response.json() as Promise<OpenAIResponse>;
529
538
  }
530
539
 
531
540
  private parseResponse(response: OpenAIResponse, requestedModel: string, rawRequest: unknown): ProviderResponse {
@@ -594,7 +603,7 @@ export class OpenAIAdapter implements ProviderAdapter {
594
603
  type: 'tool_use',
595
604
  id: tc.id,
596
605
  name: tc.function.name,
597
- input: JSON.parse(tc.function.arguments || '{}'),
606
+ input: safeParseJson(tc.function.arguments),
598
607
  });
599
608
  }
600
609
  }
@@ -690,7 +699,7 @@ export function fromOpenAIContent(message: OpenAIMessage): ContentBlock[] {
690
699
  type: 'tool_use',
691
700
  id: tc.id,
692
701
  name: tc.function.name,
693
- input: JSON.parse(tc.function.arguments || '{}'),
702
+ input: safeParseJson(tc.function.arguments),
694
703
  });
695
704
  }
696
705
  }
@@ -22,6 +22,7 @@ import {
22
22
  abortError,
23
23
  networkError,
24
24
  } from '../types/index.js';
25
+ import { safeParseJson, createCombinedSignal, SSELineParser } from './utils.js';
25
26
 
26
27
  // ============================================================================
27
28
  // Types
@@ -156,12 +157,13 @@ export class OpenRouterAdapter implements ProviderAdapter {
156
157
  openRouterRequest.stream_options = { include_usage: true };
157
158
  options?.onRequest?.(openRouterRequest);
158
159
 
160
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
159
161
  try {
160
162
  const response = await fetch(`${this.baseURL}/chat/completions`, {
161
163
  method: 'POST',
162
164
  headers: this.getHeaders(),
163
165
  body: JSON.stringify(openRouterRequest),
164
- signal: options?.signal,
166
+ signal: combinedSignal,
165
167
  });
166
168
 
167
169
  if (!response.ok) {
@@ -175,6 +177,7 @@ export class OpenRouterAdapter implements ProviderAdapter {
175
177
  }
176
178
 
177
179
  const decoder = new TextDecoder();
180
+ const sseParser = new SSELineParser();
178
181
  let accumulated = '';
179
182
  let finishReason = 'stop';
180
183
  let toolCalls: OpenRouterToolCall[] = [];
@@ -185,10 +188,9 @@ export class OpenRouterAdapter implements ProviderAdapter {
185
188
  if (done) break;
186
189
 
187
190
  const chunk = decoder.decode(value, { stream: true });
188
- const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
191
+ const dataLines = sseParser.feed(chunk);
189
192
 
190
- for (const line of lines) {
191
- const data = line.slice(6);
193
+ for (const data of dataLines) {
192
194
  if (data === '[DONE]') continue;
193
195
 
194
196
  try {
@@ -247,6 +249,8 @@ export class OpenRouterAdapter implements ProviderAdapter {
247
249
 
248
250
  } catch (error) {
249
251
  throw this.handleError(error, openRouterRequest);
252
+ } finally {
253
+ cleanup?.();
250
254
  }
251
255
  }
252
256
 
@@ -464,19 +468,24 @@ export class OpenRouterAdapter implements ProviderAdapter {
464
468
  }
465
469
 
466
470
  private async makeRequest(request: any, options?: ProviderRequestOptions): Promise<OpenRouterResponse> {
467
- const response = await fetch(`${this.baseURL}/chat/completions`, {
468
- method: 'POST',
469
- headers: this.getHeaders(),
470
- body: JSON.stringify(request),
471
- signal: options?.signal,
472
- });
473
-
474
- if (!response.ok) {
475
- const errorText = await response.text();
476
- throw new Error(`OpenRouter error: ${response.status} ${errorText}`);
471
+ const { signal: combinedSignal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
472
+ try {
473
+ const response = await fetch(`${this.baseURL}/chat/completions`, {
474
+ method: 'POST',
475
+ headers: this.getHeaders(),
476
+ body: JSON.stringify(request),
477
+ signal: combinedSignal,
478
+ });
479
+
480
+ if (!response.ok) {
481
+ const errorText = await response.text();
482
+ throw new Error(`OpenRouter error: ${response.status} ${errorText}`);
483
+ }
484
+
485
+ return await response.json() as OpenRouterResponse;
486
+ } finally {
487
+ cleanup?.();
477
488
  }
478
-
479
- return response.json() as Promise<OpenRouterResponse>;
480
489
  }
481
490
 
482
491
  private parseResponse(response: OpenRouterResponse, requestedModel: string, rawRequest: unknown): ProviderResponse {
@@ -540,7 +549,15 @@ export class OpenRouterAdapter implements ProviderAdapter {
540
549
  const content: any[] = [];
541
550
 
542
551
  if (message.content) {
543
- content.push({ type: 'text', text: message.content });
552
+ if (typeof message.content === 'string') {
553
+ content.push({ type: 'text', text: message.content });
554
+ } else if (Array.isArray(message.content)) {
555
+ for (const block of message.content) {
556
+ if (block.type === 'text') {
557
+ content.push({ type: 'text', text: block.text });
558
+ }
559
+ }
560
+ }
544
561
  }
545
562
 
546
563
  if (message.tool_calls) {
@@ -549,7 +566,7 @@ export class OpenRouterAdapter implements ProviderAdapter {
549
566
  type: 'tool_use',
550
567
  id: tc.id,
551
568
  name: tc.function.name,
552
- input: JSON.parse(tc.function.arguments || '{}'),
569
+ input: safeParseJson(tc.function.arguments),
553
570
  });
554
571
  }
555
572
  }
@@ -698,7 +715,7 @@ export function fromOpenRouterMessage(message: OpenRouterMessage): ContentBlock[
698
715
  type: 'tool_use',
699
716
  id: tc.id,
700
717
  name: tc.function.name,
701
- input: JSON.parse(tc.function.arguments || '{}'),
718
+ input: safeParseJson(tc.function.arguments),
702
719
  });
703
720
  }
704
721
  }