@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
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Safely parse a JSON string, returning an empty object on failure.
3
+ * Used for tool call arguments which may be malformed from streaming.
4
+ */
5
+ export function safeParseJson(str: string | undefined): Record<string, unknown> {
6
+ try {
7
+ return JSON.parse(str || '{}');
8
+ } catch (e) {
9
+ console.warn('[membrane] Failed to parse tool arguments JSON:', e);
10
+ return {};
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Create a combined AbortSignal that fires on either the caller's signal
16
+ * or a timeout (whichever comes first).
17
+ *
18
+ * The returned `cleanup` function MUST be called in a `finally` block to
19
+ * clear the timeout and remove the event listener, preventing leaks.
20
+ *
21
+ * Timeout aborts with `DOMException('Request timed out', 'AbortError')`
22
+ * so it classifies identically to user-initiated aborts.
23
+ */
24
+ export function createCombinedSignal(
25
+ signal?: AbortSignal,
26
+ timeoutMs?: number
27
+ ): { signal?: AbortSignal; cleanup?: () => void } {
28
+ if (!signal && !timeoutMs) return {};
29
+ if (signal && !timeoutMs) return { signal };
30
+
31
+ const controller = new AbortController();
32
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
33
+
34
+ if (timeoutMs) {
35
+ timeoutId = setTimeout(
36
+ () => controller.abort(new DOMException('Request timed out', 'AbortError')),
37
+ timeoutMs
38
+ );
39
+ }
40
+
41
+ const onAbort = () => controller.abort(signal!.reason);
42
+ if (signal) {
43
+ if (signal.aborted) {
44
+ controller.abort(signal.reason);
45
+ } else {
46
+ signal.addEventListener('abort', onAbort, { once: true });
47
+ }
48
+ }
49
+
50
+ return {
51
+ signal: controller.signal,
52
+ cleanup: () => {
53
+ if (timeoutId) clearTimeout(timeoutId);
54
+ if (signal) signal.removeEventListener('abort', onAbort);
55
+ },
56
+ };
57
+ }
58
+
59
+ /**
60
+ * SSE (Server-Sent Events) line parser that correctly handles events
61
+ * split across multiple TCP chunks.
62
+ *
63
+ * The naive approach of `chunk.split('\n').filter(l => l.startsWith('data: '))`
64
+ * silently drops events when an SSE line spans two chunks:
65
+ * Chunk 1: `data: {"choices":[{"delta":{"content":"don'` (no newline — incomplete)
66
+ * Chunk 2: `t do that"}}]}\n` (doesn't start with `data: `)
67
+ * Result: the entire event is lost, causing "skipped words" in output.
68
+ *
69
+ * This parser buffers partial lines and only yields complete `data: ...` lines.
70
+ */
71
+ export class SSELineParser {
72
+ private buffer: string = '';
73
+
74
+ /**
75
+ * Feed a raw chunk from the stream reader and get back complete SSE data lines.
76
+ * Each returned string is the content after `data: ` (e.g. the JSON payload or `[DONE]`).
77
+ */
78
+ feed(chunk: string): string[] {
79
+ this.buffer += chunk;
80
+ const results: string[] = [];
81
+
82
+ // Split on newlines, keeping the last (potentially incomplete) segment in the buffer
83
+ const lines = this.buffer.split('\n');
84
+ this.buffer = lines.pop() || '';
85
+
86
+ for (const line of lines) {
87
+ const trimmed = line.trim();
88
+ if (trimmed.startsWith('data: ')) {
89
+ results.push(trimmed.slice(6));
90
+ }
91
+ // Skip empty lines, comments (`:...`), and other SSE fields (event:, id:, retry:)
92
+ }
93
+
94
+ return results;
95
+ }
96
+
97
+ /**
98
+ * Flush any remaining buffered content (call when stream ends).
99
+ */
100
+ flush(): string[] {
101
+ if (!this.buffer.trim()) return [];
102
+ const trimmed = this.buffer.trim();
103
+ this.buffer = '';
104
+ if (trimmed.startsWith('data: ')) {
105
+ return [trimmed.slice(6)];
106
+ }
107
+ return [];
108
+ }
109
+ }
@@ -161,6 +161,15 @@ export interface NormalizedRequest {
161
161
  */
162
162
  assistantParticipant?: string;
163
163
 
164
+ /**
165
+ * Control streaming behavior when calling membrane.stream().
166
+ * - true or undefined: use streaming (default)
167
+ * - false: force non-streaming — membrane.stream() will internally use
168
+ * complete() and synthesize streaming callbacks from the full response.
169
+ * Useful for working around provider streaming bugs.
170
+ */
171
+ streaming?: boolean;
172
+
164
173
  /** Provider-specific parameters (pass-through) */
165
174
  providerParams?: Record<string, unknown>;
166
175
  }