@animalabs/membrane 0.5.69 → 0.5.70
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.
- package/dist/formatters/index.d.ts +1 -0
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +1 -0
- package/dist/formatters/index.js.map +1 -1
- package/dist/formatters/openai-responses.d.ts +23 -0
- package/dist/formatters/openai-responses.d.ts.map +1 -0
- package/dist/formatters/openai-responses.js +176 -0
- package/dist/formatters/openai-responses.js.map +1 -0
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +21 -2
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +28 -3
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +129 -0
- package/dist/providers/openai-responses-api.d.ts.map +1 -0
- package/dist/providers/openai-responses-api.js +475 -0
- package/dist/providers/openai-responses-api.js.map +1 -0
- package/package.json +1 -1
- package/src/formatters/index.ts +4 -0
- package/src/formatters/openai-responses.ts +206 -0
- package/src/membrane.ts +23 -4
- package/src/providers/anthropic.ts +29 -4
- package/src/providers/index.ts +11 -0
- package/src/providers/openai-responses-api.ts +644 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter for stateless, provider-native OpenAI Responses histories.
|
|
3
|
+
*
|
|
4
|
+
* Imported/native items are carried either in message metadata under
|
|
5
|
+
* `openaiResponsesItems` or on content blocks as `rawItem`. Those items are
|
|
6
|
+
* emitted verbatim and in order. Normalized messages created after import are
|
|
7
|
+
* converted to Responses input items without rewriting the native prefix.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ContentBlock,
|
|
12
|
+
NormalizedMessage,
|
|
13
|
+
ToolCall,
|
|
14
|
+
ToolResult,
|
|
15
|
+
} from '../types/index.js';
|
|
16
|
+
import type {
|
|
17
|
+
BuildOptions,
|
|
18
|
+
BuildResult,
|
|
19
|
+
BlockEvent,
|
|
20
|
+
ParseResult,
|
|
21
|
+
PrefillFormatter,
|
|
22
|
+
StreamEmission,
|
|
23
|
+
StreamParser,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
|
|
26
|
+
export const OPENAI_RESPONSES_ITEMS_METADATA_KEY = 'openaiResponsesItems';
|
|
27
|
+
|
|
28
|
+
class ResponsesPassthroughParser implements StreamParser {
|
|
29
|
+
private accumulated = '';
|
|
30
|
+
private blockIndex = 0;
|
|
31
|
+
private blockStarted = false;
|
|
32
|
+
|
|
33
|
+
push(chunk: string): void { this.accumulated += chunk; }
|
|
34
|
+
processChunk(chunk: string): ParseResult {
|
|
35
|
+
this.accumulated += chunk;
|
|
36
|
+
const meta = { type: 'text' as const, visible: true, blockIndex: this.blockIndex };
|
|
37
|
+
const emissions: StreamEmission[] = [];
|
|
38
|
+
const blockEvents: BlockEvent[] = [];
|
|
39
|
+
if (!this.blockStarted) {
|
|
40
|
+
const event: BlockEvent = { event: 'block_start', index: this.blockIndex, block: { type: 'text' } };
|
|
41
|
+
emissions.push({ kind: 'blockEvent', event });
|
|
42
|
+
blockEvents.push(event);
|
|
43
|
+
this.blockStarted = true;
|
|
44
|
+
}
|
|
45
|
+
emissions.push({ kind: 'content', text: chunk, meta });
|
|
46
|
+
return { emissions, content: [{ text: chunk, meta }], blockEvents };
|
|
47
|
+
}
|
|
48
|
+
flush(): ParseResult {
|
|
49
|
+
const emissions: StreamEmission[] = [];
|
|
50
|
+
const blockEvents: BlockEvent[] = [];
|
|
51
|
+
if (this.blockStarted) {
|
|
52
|
+
const event: BlockEvent = {
|
|
53
|
+
event: 'block_complete', index: this.blockIndex,
|
|
54
|
+
block: { type: 'text', content: this.accumulated },
|
|
55
|
+
};
|
|
56
|
+
emissions.push({ kind: 'blockEvent', event });
|
|
57
|
+
blockEvents.push(event);
|
|
58
|
+
this.blockStarted = false;
|
|
59
|
+
}
|
|
60
|
+
return { emissions, content: [], blockEvents };
|
|
61
|
+
}
|
|
62
|
+
getAccumulated(): string { return this.accumulated; }
|
|
63
|
+
reset(): void { this.accumulated = ''; this.blockIndex = 0; this.blockStarted = false; }
|
|
64
|
+
isInsideBlock(): boolean { return false; }
|
|
65
|
+
getCurrentBlockType() { return 'text' as const; }
|
|
66
|
+
getBlockIndex(): number { return this.blockIndex; }
|
|
67
|
+
incrementBlockIndex(): void { this.blockIndex++; }
|
|
68
|
+
getDepths() { return { functionCalls: 0, functionResults: 0, thinking: 0 }; }
|
|
69
|
+
resetForNewIteration(): void {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type NativeItem = { type?: string; id?: string; [key: string]: unknown };
|
|
73
|
+
|
|
74
|
+
export class OpenAIResponsesFormatter implements PrefillFormatter {
|
|
75
|
+
readonly name = 'openai-responses';
|
|
76
|
+
readonly usesPrefill = false;
|
|
77
|
+
|
|
78
|
+
buildMessages(messages: NormalizedMessage[], options: BuildOptions): BuildResult {
|
|
79
|
+
const items: NativeItem[] = [];
|
|
80
|
+
let hasNativeHistory = false;
|
|
81
|
+
|
|
82
|
+
for (const message of messages) {
|
|
83
|
+
const nativeItems = message.metadata?.[OPENAI_RESPONSES_ITEMS_METADATA_KEY];
|
|
84
|
+
if (Array.isArray(nativeItems)) {
|
|
85
|
+
items.push(...nativeItems as NativeItem[]);
|
|
86
|
+
hasNativeHistory = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const seenRawItems = new Set<string>();
|
|
91
|
+
let pendingParts: ContentBlock[] = [];
|
|
92
|
+
const flushPending = () => {
|
|
93
|
+
if (pendingParts.length === 0) return;
|
|
94
|
+
items.push(...this.convertBlocks(message, pendingParts, options.assistantParticipant));
|
|
95
|
+
pendingParts = [];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
for (const block of message.content) {
|
|
99
|
+
const rawItem = (block as unknown as { rawItem?: NativeItem }).rawItem;
|
|
100
|
+
if (!rawItem || typeof rawItem !== 'object') {
|
|
101
|
+
pendingParts.push(block);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
flushPending();
|
|
106
|
+
hasNativeHistory = true;
|
|
107
|
+
const key = typeof rawItem.id === 'string'
|
|
108
|
+
? `${rawItem.type ?? ''}:${rawItem.id}`
|
|
109
|
+
: JSON.stringify(rawItem);
|
|
110
|
+
if (!seenRawItems.has(key)) {
|
|
111
|
+
items.push(rawItem);
|
|
112
|
+
seenRawItems.add(key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
flushPending();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
// BuildResult's historical type says chat envelopes, but Membrane's
|
|
120
|
+
// provider boundary deliberately accepts unknown provider-native items.
|
|
121
|
+
messages: items as unknown as BuildResult['messages'],
|
|
122
|
+
// An imported rollout already contains its developer/system items.
|
|
123
|
+
// Re-injecting the recipe prompt would alter the stable prefix.
|
|
124
|
+
systemContent: hasNativeHistory ? undefined : options.systemPrompt,
|
|
125
|
+
stopSequences: options.additionalStopSequences ?? [],
|
|
126
|
+
nativeTools: options.tools?.map(tool => ({
|
|
127
|
+
type: 'function',
|
|
128
|
+
name: tool.name,
|
|
129
|
+
description: tool.description,
|
|
130
|
+
parameters: tool.inputSchema,
|
|
131
|
+
})),
|
|
132
|
+
ready: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private convertBlocks(
|
|
137
|
+
message: NormalizedMessage,
|
|
138
|
+
blocks: ContentBlock[],
|
|
139
|
+
assistantParticipant: string,
|
|
140
|
+
): NativeItem[] {
|
|
141
|
+
const isAssistant = message.participant === assistantParticipant;
|
|
142
|
+
const out: NativeItem[] = [];
|
|
143
|
+
let messageParts: unknown[] = [];
|
|
144
|
+
|
|
145
|
+
const flushMessage = () => {
|
|
146
|
+
if (messageParts.length === 0) return;
|
|
147
|
+
out.push({
|
|
148
|
+
type: 'message',
|
|
149
|
+
role: isAssistant ? 'assistant' : 'user',
|
|
150
|
+
content: messageParts,
|
|
151
|
+
});
|
|
152
|
+
messageParts = [];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for (const block of blocks) {
|
|
156
|
+
if (block.type === 'text') {
|
|
157
|
+
messageParts.push({
|
|
158
|
+
type: isAssistant ? 'output_text' : 'input_text',
|
|
159
|
+
text: block.text,
|
|
160
|
+
});
|
|
161
|
+
} else if (block.type === 'image' && !isAssistant) {
|
|
162
|
+
const source = block.source;
|
|
163
|
+
messageParts.push(source.type === 'url'
|
|
164
|
+
? { type: 'input_image', image_url: source.url }
|
|
165
|
+
: { type: 'input_image', image_url: `data:${source.mediaType};base64,${source.data}` });
|
|
166
|
+
} else if (block.type === 'tool_use') {
|
|
167
|
+
flushMessage();
|
|
168
|
+
out.push({
|
|
169
|
+
type: 'function_call',
|
|
170
|
+
call_id: block.id,
|
|
171
|
+
name: block.name,
|
|
172
|
+
arguments: JSON.stringify(block.input ?? {}),
|
|
173
|
+
});
|
|
174
|
+
} else if (block.type === 'tool_result') {
|
|
175
|
+
flushMessage();
|
|
176
|
+
out.push({
|
|
177
|
+
type: 'function_call_output',
|
|
178
|
+
call_id: block.toolUseId,
|
|
179
|
+
output: typeof block.content === 'string'
|
|
180
|
+
? block.content
|
|
181
|
+
: JSON.stringify(block.content),
|
|
182
|
+
});
|
|
183
|
+
} else if (block.type === 'redacted_thinking') {
|
|
184
|
+
flushMessage();
|
|
185
|
+
out.push({ type: 'reasoning', encrypted_content: block.data });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
flushMessage();
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
formatToolResults(results: ToolResult[]): string {
|
|
193
|
+
return JSON.stringify(results.map(result => ({
|
|
194
|
+
type: 'function_call_output',
|
|
195
|
+
call_id: result.toolUseId,
|
|
196
|
+
output: result.content,
|
|
197
|
+
})));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
createStreamParser(): StreamParser { return new ResponsesPassthroughParser(); }
|
|
201
|
+
parseToolCalls(_content: string): ToolCall[] { return []; }
|
|
202
|
+
hasToolUse(_content: string): boolean { return false; }
|
|
203
|
+
parseContentBlocks(content: string): ContentBlock[] {
|
|
204
|
+
return content ? [{ type: 'text', text: content }] : [];
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/membrane.ts
CHANGED
|
@@ -236,7 +236,7 @@ export class Membrane {
|
|
|
236
236
|
// Auto mode: choose based on formatter
|
|
237
237
|
// NativeFormatter → native tools via API
|
|
238
238
|
// AnthropicXmlFormatter (default) → XML tools in prefill
|
|
239
|
-
if (this.formatter.name === 'native') {
|
|
239
|
+
if (this.formatter.name === 'native' || this.formatter.name === 'openai-responses') {
|
|
240
240
|
return 'native';
|
|
241
241
|
}
|
|
242
242
|
|
|
@@ -986,6 +986,14 @@ export class Membrane {
|
|
|
986
986
|
request: NormalizedRequest,
|
|
987
987
|
messages: typeof request.messages
|
|
988
988
|
): any {
|
|
989
|
+
// Provider-native formatters own their complete input-item shape. The
|
|
990
|
+
// legacy implementation below is intentionally Anthropic-specific; using
|
|
991
|
+
// it for Responses would normalize away item IDs, encrypted reasoning,
|
|
992
|
+
// assistant phases, and compaction items.
|
|
993
|
+
if (this.formatter.name === 'openai-responses') {
|
|
994
|
+
return this.transformRequest({ ...request, messages }, this.formatter).providerRequest;
|
|
995
|
+
}
|
|
996
|
+
|
|
989
997
|
// Convert messages to provider format
|
|
990
998
|
const providerMessages: any[] = [];
|
|
991
999
|
|
|
@@ -1174,20 +1182,25 @@ export class Membrane {
|
|
|
1174
1182
|
const blocks: ContentBlock[] = [];
|
|
1175
1183
|
for (const item of content) {
|
|
1176
1184
|
if (item.type === 'text') {
|
|
1177
|
-
blocks.push({
|
|
1185
|
+
blocks.push({
|
|
1186
|
+
type: 'text', text: item.text,
|
|
1187
|
+
...(item.rawItem ? { rawItem: item.rawItem } : {}),
|
|
1188
|
+
} as ContentBlock);
|
|
1178
1189
|
} else if (item.type === 'tool_use') {
|
|
1179
1190
|
blocks.push({
|
|
1180
1191
|
type: 'tool_use',
|
|
1181
1192
|
id: item.id,
|
|
1182
1193
|
name: unsanitizeToolName(item.name),
|
|
1183
1194
|
input: item.input,
|
|
1184
|
-
|
|
1195
|
+
...(item.rawItem ? { rawItem: item.rawItem } : {}),
|
|
1196
|
+
} as ContentBlock);
|
|
1185
1197
|
} else if (item.type === 'thinking') {
|
|
1186
1198
|
blocks.push({
|
|
1187
1199
|
type: 'thinking',
|
|
1188
1200
|
thinking: item.thinking ?? '',
|
|
1189
1201
|
...(item.signature ? { signature: item.signature } : {}),
|
|
1190
|
-
|
|
1202
|
+
...(item.rawItem ? { rawItem: item.rawItem } : {}),
|
|
1203
|
+
} as ContentBlock);
|
|
1191
1204
|
} else if (item.type === 'redacted_thinking') {
|
|
1192
1205
|
// Pass through verbatim — carries the encrypted `data` payload
|
|
1193
1206
|
blocks.push({ ...item } as ContentBlock);
|
|
@@ -1197,6 +1210,12 @@ export class Membrane {
|
|
|
1197
1210
|
data: item.data,
|
|
1198
1211
|
mimeType: item.mimeType,
|
|
1199
1212
|
});
|
|
1213
|
+
} else if (item.rawItem) {
|
|
1214
|
+
// Opaque Responses items such as encrypted compaction or custom
|
|
1215
|
+
// tool records have no normalized ContentBlock equivalent. Retain a
|
|
1216
|
+
// zero-width carrier so Chronicle and the Responses formatter can
|
|
1217
|
+
// replay the raw item without surfacing synthetic prompt text.
|
|
1218
|
+
blocks.push({ type: 'text', text: '', rawItem: item.rawItem } as ContentBlock);
|
|
1200
1219
|
}
|
|
1201
1220
|
}
|
|
1202
1221
|
return blocks;
|
|
@@ -186,6 +186,11 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
186
186
|
let outputTokens = 0;
|
|
187
187
|
let cacheCreationTokens: number | undefined;
|
|
188
188
|
let cacheReadTokens: number | undefined;
|
|
189
|
+
let cacheCreation5mTokens: number | undefined;
|
|
190
|
+
let cacheCreation1hTokens: number | undefined;
|
|
191
|
+
let hasCacheCreationBreakdown = false;
|
|
192
|
+
let inferenceGeo: string | undefined;
|
|
193
|
+
let serviceTier: string | undefined;
|
|
189
194
|
let stopReason: string = 'end_turn';
|
|
190
195
|
let stopSequence: string | undefined;
|
|
191
196
|
let stopDetails: unknown;
|
|
@@ -207,10 +212,22 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
207
212
|
resetIdleTimer();
|
|
208
213
|
if (event.type === 'message_start') {
|
|
209
214
|
model = event.message.model;
|
|
210
|
-
const usage = event.message.usage as unknown as Record<string,
|
|
211
|
-
inputTokens = usage.input_tokens
|
|
212
|
-
cacheCreationTokens = usage.cache_creation_input_tokens
|
|
213
|
-
|
|
215
|
+
const usage = event.message.usage as unknown as Record<string, unknown>;
|
|
216
|
+
inputTokens = typeof usage.input_tokens === 'number' ? usage.input_tokens : 0;
|
|
217
|
+
cacheCreationTokens = typeof usage.cache_creation_input_tokens === 'number'
|
|
218
|
+
? usage.cache_creation_input_tokens : undefined;
|
|
219
|
+
cacheReadTokens = typeof usage.cache_read_input_tokens === 'number'
|
|
220
|
+
? usage.cache_read_input_tokens : undefined;
|
|
221
|
+
const cacheCreation = usage.cache_creation as Record<string, unknown> | undefined;
|
|
222
|
+
if (cacheCreation) {
|
|
223
|
+
hasCacheCreationBreakdown = true;
|
|
224
|
+
cacheCreation5mTokens = typeof cacheCreation.ephemeral_5m_input_tokens === 'number'
|
|
225
|
+
? cacheCreation.ephemeral_5m_input_tokens : 0;
|
|
226
|
+
cacheCreation1hTokens = typeof cacheCreation.ephemeral_1h_input_tokens === 'number'
|
|
227
|
+
? cacheCreation.ephemeral_1h_input_tokens : 0;
|
|
228
|
+
}
|
|
229
|
+
inferenceGeo = typeof usage.inference_geo === 'string' ? usage.inference_geo : undefined;
|
|
230
|
+
serviceTier = typeof usage.service_tier === 'string' ? usage.service_tier : undefined;
|
|
214
231
|
|
|
215
232
|
} else if (event.type === 'content_block_start') {
|
|
216
233
|
currentBlockIndex = event.index;
|
|
@@ -324,6 +341,14 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
324
341
|
output_tokens: outputTokens,
|
|
325
342
|
cache_creation_input_tokens: cacheCreationTokens,
|
|
326
343
|
cache_read_input_tokens: cacheReadTokens,
|
|
344
|
+
...(hasCacheCreationBreakdown ? {
|
|
345
|
+
cache_creation: {
|
|
346
|
+
ephemeral_5m_input_tokens: cacheCreation5mTokens ?? 0,
|
|
347
|
+
ephemeral_1h_input_tokens: cacheCreation1hTokens ?? 0,
|
|
348
|
+
},
|
|
349
|
+
} : {}),
|
|
350
|
+
...(inferenceGeo ? { inference_geo: inferenceGeo } : {}),
|
|
351
|
+
...(serviceTier ? { service_tier: serviceTier } : {}),
|
|
327
352
|
},
|
|
328
353
|
},
|
|
329
354
|
};
|
package/src/providers/index.ts
CHANGED
|
@@ -60,3 +60,14 @@ export {
|
|
|
60
60
|
OpenAIResponsesAdapter,
|
|
61
61
|
type OpenAIResponsesAdapterConfig,
|
|
62
62
|
} from './openai-responses.js';
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
OpenAIResponsesAPIAdapter,
|
|
66
|
+
type OpenAIResponsesAPIAdapterConfig,
|
|
67
|
+
type OpenAIResponsesAPIContentBlock,
|
|
68
|
+
type OpenAIResponsesAPIProviderResponse,
|
|
69
|
+
type OpenAIResponsesAPIRequest,
|
|
70
|
+
type OpenAIResponsesAPIResponse,
|
|
71
|
+
type OpenAIResponsesInputItem,
|
|
72
|
+
type OpenAIResponsesOutputItem,
|
|
73
|
+
} from './openai-responses-api.js';
|