@animalabs/membrane 0.5.69 → 0.5.71
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/native.d.ts.map +1 -1
- package/dist/formatters/native.js +6 -0
- package/dist/formatters/native.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 +188 -0
- package/dist/formatters/openai-responses.js.map +1 -0
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +29 -2
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +32 -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 +479 -0
- package/dist/providers/openai-responses-api.js.map +1 -0
- package/dist/types/content.d.ts +26 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js.map +1 -1
- package/package.json +1 -1
- package/src/formatters/index.ts +4 -0
- package/src/formatters/native.ts +5 -0
- package/src/formatters/openai-responses.ts +220 -0
- package/src/membrane.ts +28 -2
- package/src/providers/anthropic.ts +32 -4
- package/src/providers/index.ts +11 -0
- package/src/providers/openai-responses-api.ts +653 -0
- package/src/types/content.ts +26 -0
package/dist/types/content.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ export interface TextContent {
|
|
|
21
21
|
text: string;
|
|
22
22
|
/** Cache control for Anthropic prompt caching */
|
|
23
23
|
cache_control?: CacheControl;
|
|
24
|
+
/**
|
|
25
|
+
* Opaque provider-native item this block was derived from (e.g. an OpenAI
|
|
26
|
+
* Responses output item). Provider-native formatters replay it verbatim;
|
|
27
|
+
* other providers must ignore it. A zero-width carrier (`text: ''` plus
|
|
28
|
+
* `rawItem`) has no normalized equivalent and must be filtered out of
|
|
29
|
+
* requests for providers that reject empty text blocks (Anthropic).
|
|
30
|
+
*/
|
|
31
|
+
rawItem?: unknown;
|
|
24
32
|
}
|
|
25
33
|
export interface ImageContent {
|
|
26
34
|
type: 'image';
|
|
@@ -30,44 +38,60 @@ export interface ImageContent {
|
|
|
30
38
|
* can auto-fetch URLs from text (like Gemini 3.x) when inlineData is
|
|
31
39
|
* not viable (e.g., missing thought_signature on model-role images). */
|
|
32
40
|
sourceUrl?: string;
|
|
41
|
+
/** See {@link TextContent.rawItem}. */
|
|
42
|
+
rawItem?: unknown;
|
|
33
43
|
}
|
|
34
44
|
export interface DocumentContent {
|
|
35
45
|
type: 'document';
|
|
36
46
|
source: Base64Source;
|
|
37
47
|
filename?: string;
|
|
48
|
+
/** See {@link TextContent.rawItem}. */
|
|
49
|
+
rawItem?: unknown;
|
|
38
50
|
}
|
|
39
51
|
export interface AudioContent {
|
|
40
52
|
type: 'audio';
|
|
41
53
|
source: Base64Source;
|
|
42
54
|
duration?: number;
|
|
55
|
+
/** See {@link TextContent.rawItem}. */
|
|
56
|
+
rawItem?: unknown;
|
|
43
57
|
}
|
|
44
58
|
export interface VideoContent {
|
|
45
59
|
type: 'video';
|
|
46
60
|
source: Base64Source;
|
|
47
61
|
duration?: number;
|
|
62
|
+
/** See {@link TextContent.rawItem}. */
|
|
63
|
+
rawItem?: unknown;
|
|
48
64
|
}
|
|
49
65
|
export interface GeneratedImageContent {
|
|
50
66
|
type: 'generated_image';
|
|
51
67
|
data: string;
|
|
52
68
|
mimeType: string;
|
|
53
69
|
isPreview?: boolean;
|
|
70
|
+
/** See {@link TextContent.rawItem}. */
|
|
71
|
+
rawItem?: unknown;
|
|
54
72
|
}
|
|
55
73
|
export interface ToolUseContent {
|
|
56
74
|
type: 'tool_use';
|
|
57
75
|
id: string;
|
|
58
76
|
name: string;
|
|
59
77
|
input: Record<string, unknown>;
|
|
78
|
+
/** See {@link TextContent.rawItem}. */
|
|
79
|
+
rawItem?: unknown;
|
|
60
80
|
}
|
|
61
81
|
export interface ToolResultContent {
|
|
62
82
|
type: 'tool_result';
|
|
63
83
|
toolUseId: string;
|
|
64
84
|
content: string | ContentBlock[];
|
|
65
85
|
isError?: boolean;
|
|
86
|
+
/** See {@link TextContent.rawItem}. */
|
|
87
|
+
rawItem?: unknown;
|
|
66
88
|
}
|
|
67
89
|
export interface ThinkingContent {
|
|
68
90
|
type: 'thinking';
|
|
69
91
|
thinking: string;
|
|
70
92
|
signature?: string;
|
|
93
|
+
/** See {@link TextContent.rawItem}. */
|
|
94
|
+
rawItem?: unknown;
|
|
71
95
|
}
|
|
72
96
|
export interface RedactedThinkingContent {
|
|
73
97
|
type: 'redacted_thinking';
|
|
@@ -77,6 +101,8 @@ export interface RedactedThinkingContent {
|
|
|
77
101
|
* (the API decrypts it to reconstruct prior reasoning).
|
|
78
102
|
*/
|
|
79
103
|
data: string;
|
|
104
|
+
/** See {@link TextContent.rawItem}. */
|
|
105
|
+
rawItem?: unknown;
|
|
80
106
|
}
|
|
81
107
|
export type ContentBlock = TextContent | ImageContent | DocumentContent | AudioContent | VideoContent | GeneratedImageContent | ToolUseContent | ToolResultContent | ThinkingContent | RedactedThinkingContent;
|
|
82
108
|
export declare function isTextContent(block: ContentBlock): block is TextContent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,kEAAkE;IAClE,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACnB;AAMD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,SAAS,CAAC;AAMnD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,aAAa,CAAC,EAAE,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAAA;;GAEG;AAwKH,+EAA+E;AAC/E,cAAc;AACd,+EAA+E;AAE/E,MAAM,UAAU,aAAa,CAAC,KAAmB;IAC/C,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAmB;IAChD,OAAO,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAmB;IACnD,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAmB;IAChD,OAAO,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAmB;IAChD,OAAO,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAmB;IACzD,OAAO,KAAK,CAAC,IAAI,KAAK,iBAAiB,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAmB;IACrD,OAAO,KAAK,CAAC,IAAI,KAAK,aAAa,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAmB;IACnD,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,KAAmB;IAC3D,OAAO,KAAK,CAAC,IAAI,KAAK,mBAAmB,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,KAAmB;IAEnB,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC"}
|
package/package.json
CHANGED
package/src/formatters/index.ts
CHANGED
|
@@ -16,6 +16,10 @@ export type {
|
|
|
16
16
|
|
|
17
17
|
export { AnthropicXmlFormatter, type AnthropicXmlFormatterConfig } from './anthropic-xml.js';
|
|
18
18
|
export { NativeFormatter, type NativeFormatterConfig } from './native.js';
|
|
19
|
+
export {
|
|
20
|
+
OpenAIResponsesFormatter,
|
|
21
|
+
OPENAI_RESPONSES_ITEMS_METADATA_KEY,
|
|
22
|
+
} from './openai-responses.js';
|
|
19
23
|
export { CompletionsFormatter, type CompletionsFormatterConfig } from './completions.js';
|
|
20
24
|
|
|
21
25
|
export {
|
package/src/formatters/native.ts
CHANGED
|
@@ -395,6 +395,11 @@ export class NativeFormatter implements PrefillFormatter {
|
|
|
395
395
|
|
|
396
396
|
for (const block of content) {
|
|
397
397
|
if (block.type === 'text') {
|
|
398
|
+
// Empty text blocks are rejected by the Anthropic API. In particular,
|
|
399
|
+
// zero-width rawItem carriers (opaque provider-native items smuggled
|
|
400
|
+
// through normalized history) must not leak here. Filter BEFORE the
|
|
401
|
+
// name prefix below would make them non-empty.
|
|
402
|
+
if (block.text === '') continue;
|
|
398
403
|
let text = block.text;
|
|
399
404
|
if (options.includeNames) {
|
|
400
405
|
const prefix = this.config.nameFormat.replace('{name}', participant);
|
|
@@ -0,0 +1,220 @@
|
|
|
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 hasImportedItems = 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
|
+
hasImportedItems = 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.rawItem as NativeItem | undefined;
|
|
100
|
+
if (!rawItem || typeof rawItem !== 'object') {
|
|
101
|
+
pendingParts.push(block);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
flushPending();
|
|
106
|
+
const key = typeof rawItem.id === 'string'
|
|
107
|
+
? `${rawItem.type ?? ''}:${rawItem.id}`
|
|
108
|
+
: JSON.stringify(rawItem);
|
|
109
|
+
if (!seenRawItems.has(key)) {
|
|
110
|
+
items.push(rawItem);
|
|
111
|
+
seenRawItems.add(key);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
flushPending();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// An imported rollout already carries its own developer/system items, and
|
|
118
|
+
// re-injecting the recipe prompt over them would double the instructions.
|
|
119
|
+
// Suppress the system prompt ONLY for imported histories — signalled by
|
|
120
|
+
// the import metadata key or by a developer/system item in the native
|
|
121
|
+
// prefix. Blocks with `rawItem` do NOT count: every response from this
|
|
122
|
+
// provider attaches rawItem (see parseProviderContent), so keying on it
|
|
123
|
+
// would silently drop a fresh session's system prompt from turn 2 onward
|
|
124
|
+
// (turn 1 sends it as the top-level `instructions` request field, never
|
|
125
|
+
// as an input item, and the adapter is stateless — nothing else retains
|
|
126
|
+
// it). `instructions` is a separate request field, so re-sending it does
|
|
127
|
+
// not perturb the input-item prefix or its caching.
|
|
128
|
+
const hasImportedSystemItem = hasImportedItems ||
|
|
129
|
+
items.some(item =>
|
|
130
|
+
(item.type === 'message' || item.type === undefined) &&
|
|
131
|
+
((item as { role?: unknown }).role === 'developer' ||
|
|
132
|
+
(item as { role?: unknown }).role === 'system'));
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
// BuildResult's historical type says chat envelopes, but Membrane's
|
|
136
|
+
// provider boundary deliberately accepts unknown provider-native items.
|
|
137
|
+
messages: items as unknown as BuildResult['messages'],
|
|
138
|
+
systemContent: hasImportedSystemItem ? undefined : options.systemPrompt,
|
|
139
|
+
stopSequences: options.additionalStopSequences ?? [],
|
|
140
|
+
nativeTools: options.tools?.map(tool => ({
|
|
141
|
+
type: 'function',
|
|
142
|
+
name: tool.name,
|
|
143
|
+
description: tool.description,
|
|
144
|
+
parameters: tool.inputSchema,
|
|
145
|
+
})),
|
|
146
|
+
ready: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private convertBlocks(
|
|
151
|
+
message: NormalizedMessage,
|
|
152
|
+
blocks: ContentBlock[],
|
|
153
|
+
assistantParticipant: string,
|
|
154
|
+
): NativeItem[] {
|
|
155
|
+
const isAssistant = message.participant === assistantParticipant;
|
|
156
|
+
const out: NativeItem[] = [];
|
|
157
|
+
let messageParts: unknown[] = [];
|
|
158
|
+
|
|
159
|
+
const flushMessage = () => {
|
|
160
|
+
if (messageParts.length === 0) return;
|
|
161
|
+
out.push({
|
|
162
|
+
type: 'message',
|
|
163
|
+
role: isAssistant ? 'assistant' : 'user',
|
|
164
|
+
content: messageParts,
|
|
165
|
+
});
|
|
166
|
+
messageParts = [];
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
for (const block of blocks) {
|
|
170
|
+
if (block.type === 'text') {
|
|
171
|
+
messageParts.push({
|
|
172
|
+
type: isAssistant ? 'output_text' : 'input_text',
|
|
173
|
+
text: block.text,
|
|
174
|
+
});
|
|
175
|
+
} else if (block.type === 'image' && !isAssistant) {
|
|
176
|
+
const source = block.source;
|
|
177
|
+
messageParts.push(source.type === 'url'
|
|
178
|
+
? { type: 'input_image', image_url: source.url }
|
|
179
|
+
: { type: 'input_image', image_url: `data:${source.mediaType};base64,${source.data}` });
|
|
180
|
+
} else if (block.type === 'tool_use') {
|
|
181
|
+
flushMessage();
|
|
182
|
+
out.push({
|
|
183
|
+
type: 'function_call',
|
|
184
|
+
call_id: block.id,
|
|
185
|
+
name: block.name,
|
|
186
|
+
arguments: JSON.stringify(block.input ?? {}),
|
|
187
|
+
});
|
|
188
|
+
} else if (block.type === 'tool_result') {
|
|
189
|
+
flushMessage();
|
|
190
|
+
out.push({
|
|
191
|
+
type: 'function_call_output',
|
|
192
|
+
call_id: block.toolUseId,
|
|
193
|
+
output: typeof block.content === 'string'
|
|
194
|
+
? block.content
|
|
195
|
+
: JSON.stringify(block.content),
|
|
196
|
+
});
|
|
197
|
+
} else if (block.type === 'redacted_thinking') {
|
|
198
|
+
flushMessage();
|
|
199
|
+
out.push({ type: 'reasoning', encrypted_content: block.data });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
flushMessage();
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
formatToolResults(results: ToolResult[]): string {
|
|
207
|
+
return JSON.stringify(results.map(result => ({
|
|
208
|
+
type: 'function_call_output',
|
|
209
|
+
call_id: result.toolUseId,
|
|
210
|
+
output: result.content,
|
|
211
|
+
})));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
createStreamParser(): StreamParser { return new ResponsesPassthroughParser(); }
|
|
215
|
+
parseToolCalls(_content: string): ToolCall[] { return []; }
|
|
216
|
+
hasToolUse(_content: string): boolean { return false; }
|
|
217
|
+
parseContentBlocks(content: string): ContentBlock[] {
|
|
218
|
+
return content ? [{ type: 'text', text: content }] : [];
|
|
219
|
+
}
|
|
220
|
+
}
|
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
|
|
|
@@ -1012,6 +1020,11 @@ export class Membrane {
|
|
|
1012
1020
|
const includeNamePrefix = !isAssistant;
|
|
1013
1021
|
for (const block of msg.content) {
|
|
1014
1022
|
if (block.type === 'text') {
|
|
1023
|
+
// Empty text blocks are rejected by the Anthropic API. In
|
|
1024
|
+
// particular, zero-width rawItem carriers (opaque Responses items,
|
|
1025
|
+
// see parseProviderContent) must not leak here. Filter BEFORE the
|
|
1026
|
+
// name prefix below would make them non-empty.
|
|
1027
|
+
if (block.text === '') continue;
|
|
1015
1028
|
let text = block.text;
|
|
1016
1029
|
if (includeNamePrefix && msg.participant) {
|
|
1017
1030
|
text = `${msg.participant}: ${text}`;
|
|
@@ -1174,19 +1187,24 @@ export class Membrane {
|
|
|
1174
1187
|
const blocks: ContentBlock[] = [];
|
|
1175
1188
|
for (const item of content) {
|
|
1176
1189
|
if (item.type === 'text') {
|
|
1177
|
-
blocks.push({
|
|
1190
|
+
blocks.push({
|
|
1191
|
+
type: 'text', text: item.text,
|
|
1192
|
+
...(item.rawItem ? { rawItem: item.rawItem } : {}),
|
|
1193
|
+
});
|
|
1178
1194
|
} else if (item.type === 'tool_use') {
|
|
1179
1195
|
blocks.push({
|
|
1180
1196
|
type: 'tool_use',
|
|
1181
1197
|
id: item.id,
|
|
1182
1198
|
name: unsanitizeToolName(item.name),
|
|
1183
1199
|
input: item.input,
|
|
1200
|
+
...(item.rawItem ? { rawItem: item.rawItem } : {}),
|
|
1184
1201
|
});
|
|
1185
1202
|
} else if (item.type === 'thinking') {
|
|
1186
1203
|
blocks.push({
|
|
1187
1204
|
type: 'thinking',
|
|
1188
1205
|
thinking: item.thinking ?? '',
|
|
1189
1206
|
...(item.signature ? { signature: item.signature } : {}),
|
|
1207
|
+
...(item.rawItem ? { rawItem: item.rawItem } : {}),
|
|
1190
1208
|
});
|
|
1191
1209
|
} else if (item.type === 'redacted_thinking') {
|
|
1192
1210
|
// Pass through verbatim — carries the encrypted `data` payload
|
|
@@ -1197,6 +1215,14 @@ export class Membrane {
|
|
|
1197
1215
|
data: item.data,
|
|
1198
1216
|
mimeType: item.mimeType,
|
|
1199
1217
|
});
|
|
1218
|
+
} else if (item.rawItem) {
|
|
1219
|
+
// Opaque Responses items such as encrypted compaction or custom
|
|
1220
|
+
// tool records have no normalized ContentBlock equivalent. Retain a
|
|
1221
|
+
// zero-width carrier so Chronicle and the Responses formatter can
|
|
1222
|
+
// replay the raw item without surfacing synthetic prompt text.
|
|
1223
|
+
// Anthropic-bound conversion paths filter these out (empty text
|
|
1224
|
+
// blocks are a 400 there); the Responses formatter replays rawItem.
|
|
1225
|
+
blocks.push({ type: 'text', text: '', rawItem: item.rawItem });
|
|
1200
1226
|
}
|
|
1201
1227
|
}
|
|
1202
1228
|
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
|
};
|
|
@@ -661,6 +686,9 @@ export function toAnthropicContent(blocks: ContentBlock[]): Anthropic.ContentBlo
|
|
|
661
686
|
for (const block of blocks) {
|
|
662
687
|
switch (block.type) {
|
|
663
688
|
case 'text': {
|
|
689
|
+
// Empty text blocks (including zero-width rawItem carriers for opaque
|
|
690
|
+
// provider-native items) are rejected by the Anthropic API — drop them.
|
|
691
|
+
if (block.text === '') break;
|
|
664
692
|
const textBlock: any = { type: 'text', text: block.text };
|
|
665
693
|
// Preserve cache_control if present
|
|
666
694
|
if (block.cache_control) {
|
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';
|