@animalabs/membrane 0.5.80 → 0.5.82
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/cache-wire-receipt.d.ts +13 -0
- package/dist/cache-wire-receipt.d.ts.map +1 -0
- package/dist/cache-wire-receipt.js +108 -0
- package/dist/cache-wire-receipt.js.map +1 -0
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/process.d.ts +3 -1
- package/dist/context/process.d.ts.map +1 -1
- package/dist/context/process.js +230 -49
- package/dist/context/process.js.map +1 -1
- package/dist/context/types.d.ts +30 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +16 -0
- package/dist/context/types.js.map +1 -1
- package/dist/floating-cache-marker.test.js +21 -0
- package/dist/floating-cache-marker.test.js.map +1 -1
- package/dist/formatters/anthropic-xml.d.ts +2 -0
- package/dist/formatters/anthropic-xml.d.ts.map +1 -1
- package/dist/formatters/anthropic-xml.js +64 -23
- package/dist/formatters/anthropic-xml.js.map +1 -1
- package/dist/formatters/native.d.ts.map +1 -1
- package/dist/formatters/native.js +16 -3
- package/dist/formatters/native.js.map +1 -1
- package/dist/formatters/normalize-tool-pairs.d.ts +7 -0
- package/dist/formatters/normalize-tool-pairs.d.ts.map +1 -1
- package/dist/formatters/normalize-tool-pairs.js +45 -0
- package/dist/formatters/normalize-tool-pairs.js.map +1 -1
- package/dist/formatters/types.d.ts +31 -0
- package/dist/formatters/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/membrane.d.ts +137 -8
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +823 -310
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +142 -25
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts +8 -0
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +24 -1
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/providers/gemini.d.ts +11 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +104 -82
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +2 -0
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts +7 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +99 -48
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +5 -0
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +107 -44
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +16 -0
- package/dist/providers/openai-responses-api.d.ts.map +1 -1
- package/dist/providers/openai-responses-api.js +38 -10
- package/dist/providers/openai-responses-api.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +2 -0
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +8 -1
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -44
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +9 -0
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +118 -62
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/utils.d.ts +74 -2
- package/dist/providers/utils.d.ts.map +1 -1
- package/dist/providers/utils.js +184 -3
- package/dist/providers/utils.js.map +1 -1
- package/dist/registry/default-pricing.d.ts +22 -0
- package/dist/registry/default-pricing.d.ts.map +1 -1
- package/dist/registry/default-pricing.js +162 -28
- package/dist/registry/default-pricing.js.map +1 -1
- package/dist/types/content.d.ts +13 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js.map +1 -1
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +26 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/provider.d.ts +50 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/request.d.ts +6 -0
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/response.d.ts +85 -4
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/response.js.map +1 -1
- package/dist/types/streaming.d.ts +6 -1
- package/dist/types/streaming.d.ts.map +1 -1
- package/dist/utils/cache-marker-budget.d.ts +91 -0
- package/dist/utils/cache-marker-budget.d.ts.map +1 -0
- package/dist/utils/cache-marker-budget.js +208 -0
- package/dist/utils/cache-marker-budget.js.map +1 -0
- package/dist/utils/cost.d.ts +22 -0
- package/dist/utils/cost.d.ts.map +1 -1
- package/dist/utils/cost.js +55 -0
- package/dist/utils/cost.js.map +1 -1
- package/dist/utils/thinking-carriers.d.ts +80 -0
- package/dist/utils/thinking-carriers.d.ts.map +1 -0
- package/dist/utils/thinking-carriers.js +115 -0
- package/dist/utils/thinking-carriers.js.map +1 -0
- package/dist/utils/tool-parser.d.ts +24 -0
- package/dist/utils/tool-parser.d.ts.map +1 -1
- package/dist/utils/tool-parser.js +367 -161
- package/dist/utils/tool-parser.js.map +1 -1
- package/dist/utils/usage.d.ts +71 -0
- package/dist/utils/usage.d.ts.map +1 -0
- package/dist/utils/usage.js +161 -0
- package/dist/utils/usage.js.map +1 -0
- package/dist/yielding-stream.d.ts +8 -0
- package/dist/yielding-stream.d.ts.map +1 -1
- package/dist/yielding-stream.js +49 -3
- package/dist/yielding-stream.js.map +1 -1
- package/package.json +1 -1
- package/src/cache-wire-receipt.ts +125 -0
- package/src/context/index.ts +1 -0
- package/src/context/process.ts +305 -59
- package/src/context/types.ts +38 -0
- package/src/floating-cache-marker.test.ts +24 -0
- package/src/formatters/anthropic-xml.ts +66 -23
- package/src/formatters/native.ts +23 -2
- package/src/formatters/normalize-tool-pairs.ts +49 -0
- package/src/formatters/types.ts +35 -0
- package/src/index.ts +6 -0
- package/src/membrane.ts +974 -313
- package/src/providers/anthropic.ts +173 -19
- package/src/providers/bedrock.ts +25 -1
- package/src/providers/gemini.ts +125 -87
- package/src/providers/index.ts +1 -0
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-compatible.ts +102 -45
- package/src/providers/openai-completions.ts +112 -46
- package/src/providers/openai-responses-api.ts +45 -15
- package/src/providers/openai-responses.ts +8 -1
- package/src/providers/openai.ts +95 -42
- package/src/providers/openrouter.ts +125 -63
- package/src/providers/utils.ts +227 -6
- package/src/registry/default-pricing.ts +165 -28
- package/src/types/content.ts +14 -0
- package/src/types/errors.ts +28 -0
- package/src/types/index.ts +6 -0
- package/src/types/provider.ts +55 -0
- package/src/types/request.ts +7 -0
- package/src/types/response.ts +109 -8
- package/src/types/streaming.ts +8 -2
- package/src/utils/cache-marker-budget.ts +237 -0
- package/src/utils/cost.ts +64 -0
- package/src/utils/thinking-carriers.ts +125 -0
- package/src/utils/tool-parser.ts +473 -174
- package/src/utils/usage.ts +184 -0
- package/src/yielding-stream.ts +54 -3
package/src/context/types.ts
CHANGED
|
@@ -66,10 +66,36 @@ export interface ContextConfig {
|
|
|
66
66
|
preferUserMessages?: boolean;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Participant name this deployment's assistant speaks as (membrane's own
|
|
71
|
+
* `assistantParticipant`). Used by the `preferUserMessages` adjustment to
|
|
72
|
+
* tell an assistant turn from a user turn. When unset, the legacy name
|
|
73
|
+
* list (`claude`/`assistant`/`bot`/`ai`) is used.
|
|
74
|
+
*/
|
|
75
|
+
assistantParticipant?: string;
|
|
76
|
+
|
|
69
77
|
/** Custom token estimator (default: chars / 4) */
|
|
70
78
|
tokenEstimator?: (message: NormalizedMessage) => number;
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Thrown by `processContext` when messages do not carry stable identity.
|
|
83
|
+
*
|
|
84
|
+
* The module keys continuity detection, marker stability and the
|
|
85
|
+
* `cachedStartMessageId` fetch anchor off `metadata.sourceId`. Without it
|
|
86
|
+
* every call sees a brand-new conversation, so rolling and caching are
|
|
87
|
+
* silently disabled — the module refuses rather than degrade invisibly.
|
|
88
|
+
*/
|
|
89
|
+
export class MembraneContextIdentityError extends Error {
|
|
90
|
+
constructor(
|
|
91
|
+
message: string,
|
|
92
|
+
public readonly messageIndices: readonly number[]
|
|
93
|
+
) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = 'MembraneContextIdentityError';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
// ============================================================================
|
|
74
100
|
// Context State (persisted between calls)
|
|
75
101
|
// ============================================================================
|
|
@@ -155,6 +181,18 @@ export interface ContextInfo {
|
|
|
155
181
|
|
|
156
182
|
/** Whether hard limit was hit */
|
|
157
183
|
hardLimitHit: boolean;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Set when the window still exceeds a limit after truncation. The kept
|
|
187
|
+
* window is floored at one message, so a single oversize message cannot
|
|
188
|
+
* be truncated away — it is reported here instead of shipping an empty
|
|
189
|
+
* `messages` array.
|
|
190
|
+
*/
|
|
191
|
+
residualOverflow?: {
|
|
192
|
+
unit: 'characters' | 'tokens' | 'messages';
|
|
193
|
+
limit: number;
|
|
194
|
+
actual: number;
|
|
195
|
+
};
|
|
158
196
|
|
|
159
197
|
/**
|
|
160
198
|
* First message ID of the cached window.
|
|
@@ -151,6 +151,30 @@ describe('floating cache marker', () => {
|
|
|
151
151
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
it('warns again after the rate-limit interval instead of latching for the process lifetime', () => {
|
|
155
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
156
|
+
const clock = vi.spyOn(Date, 'now');
|
|
157
|
+
clock.mockReturnValue(1_000_000);
|
|
158
|
+
|
|
159
|
+
const messages = turn(3);
|
|
160
|
+
for (const msg of messages) {
|
|
161
|
+
if ((msg.content[0] as any).type === 'tool_result') (msg as any).cacheBreakpoint = true;
|
|
162
|
+
}
|
|
163
|
+
const membrane = new Membrane({ name: 'anthropic' } as any);
|
|
164
|
+
|
|
165
|
+
build(makeRequest(), messages, true, membrane);
|
|
166
|
+
build(makeRequest(), messages, true, membrane);
|
|
167
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
168
|
+
|
|
169
|
+
// A long-lived Membrane must not go permanently quiet about an
|
|
170
|
+
// over-budget wire: past the interval the condition reports again, and
|
|
171
|
+
// says how many occurrences it swallowed meanwhile.
|
|
172
|
+
clock.mockReturnValue(1_000_000 + 61_000);
|
|
173
|
+
build(makeRequest(), messages, true, membrane);
|
|
174
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
175
|
+
expect(String(warn.mock.calls[1]![0])).toContain('1 further occurrences suppressed');
|
|
176
|
+
});
|
|
177
|
+
|
|
154
178
|
it('floats from the residuum left after the tools/system fallback on markerless requests', () => {
|
|
155
179
|
const pr = build(makeRequest(), turn(2, false), true);
|
|
156
180
|
const m = markers(pr);
|
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
type ToolDefinitionForPrompt,
|
|
35
35
|
} from '../utils/tool-parser.js';
|
|
36
36
|
import { IncrementalXmlParser } from '../utils/stream-parser.js';
|
|
37
|
+
import { assertCacheMarkersWithinLimit, clampCacheMarkers } from '../utils/cache-marker-budget.js';
|
|
38
|
+
import { lastCacheableBlockIndex } from './native.js';
|
|
37
39
|
import { isAcceptedImageMediaType, strippedImagePlaceholder } from '../utils/image-media.js';
|
|
38
40
|
|
|
39
41
|
// ============================================================================
|
|
@@ -118,9 +120,13 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
118
120
|
readonly name = 'anthropic-xml';
|
|
119
121
|
readonly usesPrefill = true;
|
|
120
122
|
|
|
123
|
+
/** See PrefillFormatter.configuredToolMode — undefined when the caller left the mode to Membrane. */
|
|
124
|
+
readonly configuredToolMode: 'xml' | 'native' | undefined;
|
|
125
|
+
|
|
121
126
|
private config: Required<AnthropicXmlFormatterConfig>;
|
|
122
127
|
|
|
123
128
|
constructor(config: AnthropicXmlFormatterConfig = {}) {
|
|
129
|
+
this.configuredToolMode = config.toolMode;
|
|
124
130
|
this.config = {
|
|
125
131
|
toolMode: config.toolMode ?? 'xml',
|
|
126
132
|
toolInjectionMode: config.toolInjectionMode ?? 'conversation',
|
|
@@ -143,12 +149,17 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
143
149
|
thinking,
|
|
144
150
|
systemPrompt,
|
|
145
151
|
promptCaching = false,
|
|
152
|
+
cacheMarkers = 'membrane-system',
|
|
146
153
|
cacheTtl,
|
|
147
154
|
contextPrefix,
|
|
148
155
|
prefillUserMessage,
|
|
149
156
|
hasCacheMarker,
|
|
150
157
|
} = options;
|
|
151
158
|
|
|
159
|
+
// Membrane resolves the mode per request and passes it here; the
|
|
160
|
+
// constructor-time mode is the fallback for direct callers only.
|
|
161
|
+
const toolMode = options.toolMode ?? this.config.toolMode;
|
|
162
|
+
|
|
152
163
|
// Build cache_control object (with optional TTL for extended caching)
|
|
153
164
|
const cacheControl: Record<string, unknown> = { type: 'ephemeral' };
|
|
154
165
|
if (cacheTtl) {
|
|
@@ -165,42 +176,66 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
165
176
|
// message continues the same turn, so it must not get a fresh label.
|
|
166
177
|
let lastWasToolResults = false;
|
|
167
178
|
|
|
168
|
-
// Track cache markers applied
|
|
169
|
-
let cacheMarkersApplied = 0;
|
|
170
179
|
|
|
171
180
|
// Calculate tool injection point
|
|
172
181
|
const totalMessages = messages.length;
|
|
173
182
|
const toolInjectionIndex = Math.max(0, totalMessages - this.config.toolInjectionPosition);
|
|
174
183
|
let toolsInjected = false;
|
|
175
184
|
const hasToolsForConversation =
|
|
176
|
-
|
|
185
|
+
toolMode === 'xml' &&
|
|
177
186
|
this.config.toolInjectionMode === 'conversation' &&
|
|
178
187
|
tools &&
|
|
179
188
|
tools.length > 0;
|
|
180
189
|
const toolsText = hasToolsForConversation ? this.formatToolsForInjection(tools!) : '';
|
|
181
190
|
|
|
182
|
-
// Build system content
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
// Build system content. A caller-supplied system ARRAY keeps its block
|
|
192
|
+
// structure and its per-block cache_control: `request.system` explicitly
|
|
193
|
+
// accepts caller-marked blocks, and flattening them into one text block
|
|
194
|
+
// discarded every marker the caller placed (three in, one out) — the
|
|
195
|
+
// caller's stable prefixes then re-paid full input price forever.
|
|
196
|
+
const callerSystemBlocks = Array.isArray(systemPrompt)
|
|
197
|
+
? systemPrompt
|
|
198
|
+
.filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text')
|
|
199
|
+
.map((b) => {
|
|
200
|
+
const callerMarker = (b as unknown as { cache_control?: unknown }).cache_control;
|
|
201
|
+
const block: Record<string, unknown> = { type: 'text', text: b.text };
|
|
202
|
+
if (callerMarker) block.cache_control = callerMarker;
|
|
203
|
+
return block;
|
|
204
|
+
})
|
|
205
|
+
: undefined;
|
|
206
|
+
const systemBlocks = callerSystemBlocks?.length ? callerSystemBlocks : undefined;
|
|
207
|
+
|
|
208
|
+
let systemText = typeof systemPrompt === 'string'
|
|
209
|
+
? systemPrompt
|
|
210
|
+
: (systemBlocks?.map((b) => b.text as string).join('\n') ?? '');
|
|
190
211
|
|
|
191
212
|
// Inject tools into system if configured
|
|
192
|
-
if (
|
|
213
|
+
if (toolMode === 'xml' && this.config.toolInjectionMode === 'system' && tools?.length) {
|
|
193
214
|
const toolsXml = this.formatToolDefinitionsXml(tools);
|
|
194
215
|
systemText = this.injectToolsIntoSystem(systemText, toolsXml);
|
|
216
|
+
if (systemBlocks) {
|
|
217
|
+
// Append to the LAST block only — appending to the join would
|
|
218
|
+
// collapse the array and take every earlier block's marker with it.
|
|
219
|
+
const tail = systemBlocks[systemBlocks.length - 1]!;
|
|
220
|
+
tail.text = this.injectToolsIntoSystem(tail.text as string, toolsXml);
|
|
221
|
+
}
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
// Build system content with optional cache control
|
|
198
225
|
let systemContent: unknown;
|
|
199
|
-
if (
|
|
226
|
+
if (systemBlocks) {
|
|
227
|
+
// The caller's own markers are authoritative: adding one beside them
|
|
228
|
+
// spends a slot the caller already allocated.
|
|
229
|
+
const callerMarkedAny = systemBlocks.some((b) => b.cache_control);
|
|
230
|
+
if (promptCaching && !callerMarkedAny) {
|
|
231
|
+
const bpIdx = lastCacheableBlockIndex(systemBlocks);
|
|
232
|
+
if (bpIdx >= 0) systemBlocks[bpIdx]!.cache_control = cacheControl;
|
|
233
|
+
}
|
|
234
|
+
systemContent = systemBlocks;
|
|
235
|
+
} else if (systemText) {
|
|
200
236
|
const systemBlock: Record<string, unknown> = { type: 'text', text: systemText };
|
|
201
|
-
if (promptCaching) {
|
|
237
|
+
if (promptCaching && cacheMarkers === 'membrane-system') {
|
|
202
238
|
systemBlock.cache_control = cacheControl;
|
|
203
|
-
cacheMarkersApplied++;
|
|
204
239
|
}
|
|
205
240
|
systemContent = [systemBlock];
|
|
206
241
|
}
|
|
@@ -208,9 +243,8 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
208
243
|
// Add context prefix as first cached assistant message (for simulacrum seeding)
|
|
209
244
|
if (contextPrefix) {
|
|
210
245
|
const prefixBlock: Record<string, unknown> = { type: 'text', text: contextPrefix };
|
|
211
|
-
if (promptCaching) {
|
|
246
|
+
if (promptCaching && cacheMarkers === 'membrane-system') {
|
|
212
247
|
prefixBlock.cache_control = cacheControl;
|
|
213
|
-
cacheMarkersApplied++;
|
|
214
248
|
}
|
|
215
249
|
providerMessages.push({
|
|
216
250
|
role: 'assistant',
|
|
@@ -276,7 +310,6 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
276
310
|
const content = currentConversation.join(joiner);
|
|
277
311
|
const contentBlock: Record<string, unknown> = { type: 'text', text: content };
|
|
278
312
|
contentBlock.cache_control = cacheControl;
|
|
279
|
-
cacheMarkersApplied++;
|
|
280
313
|
providerMessages.push({
|
|
281
314
|
role: 'assistant',
|
|
282
315
|
content: [contentBlock],
|
|
@@ -344,7 +377,6 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
344
377
|
const content = currentConversation.join(joiner);
|
|
345
378
|
const contentBlock: Record<string, unknown> = { type: 'text', text: content };
|
|
346
379
|
contentBlock.cache_control = cacheControl;
|
|
347
|
-
cacheMarkersApplied++;
|
|
348
380
|
providerMessages.push({
|
|
349
381
|
role: 'assistant',
|
|
350
382
|
content: [contentBlock],
|
|
@@ -394,9 +426,8 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
394
426
|
type: 'text',
|
|
395
427
|
text: 'The assistant is in CLI simulation mode, and responds to the user\'s CLI commands only with the output of the command.',
|
|
396
428
|
};
|
|
397
|
-
if (promptCaching) {
|
|
429
|
+
if (promptCaching && cacheMarkers === 'membrane-system') {
|
|
398
430
|
cliSystemBlock.cache_control = cacheControl;
|
|
399
|
-
cacheMarkersApplied++;
|
|
400
431
|
}
|
|
401
432
|
systemContent = [cliSystemBlock];
|
|
402
433
|
providerMessages.unshift({
|
|
@@ -416,10 +447,22 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
416
447
|
const stopSequences = this.buildStopSequences(messages, assistantParticipant, options);
|
|
417
448
|
|
|
418
449
|
// Native tools if configured
|
|
419
|
-
const nativeTools =
|
|
450
|
+
const nativeTools = toolMode === 'native' && tools?.length
|
|
420
451
|
? this.convertToNativeTools(tools)
|
|
421
452
|
: undefined;
|
|
422
453
|
|
|
454
|
+
// Budget. Five sites above attach cache_control (system, contextPrefix,
|
|
455
|
+
// hasCacheMarker flush, cacheBreakpoint flush, CLI-simulation system) and
|
|
456
|
+
// multiple cacheBreakpoints are documented input, so a prefill turn with
|
|
457
|
+
// three marked messages reaches five markers — one over Anthropic's hard
|
|
458
|
+
// limit, which rejects the request outright. Clamping here, once, on the
|
|
459
|
+
// finished artifacts is the only count that can see all five sites; the
|
|
460
|
+
// reported tally is that same recount, so it can never drift from the wire.
|
|
461
|
+
const cacheSurfaces = { messages: providerMessages, system: systemContent, tools: nativeTools };
|
|
462
|
+
const budget = cacheMarkers === 'cm-owned'
|
|
463
|
+
? { total: assertCacheMarkersWithinLimit(cacheSurfaces, 'anthropic-xml') }
|
|
464
|
+
: clampCacheMarkers(cacheSurfaces, 'anthropic-xml');
|
|
465
|
+
|
|
423
466
|
return {
|
|
424
467
|
messages: providerMessages,
|
|
425
468
|
systemContent,
|
|
@@ -428,7 +471,7 @@ export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
|
428
471
|
: undefined,
|
|
429
472
|
stopSequences,
|
|
430
473
|
nativeTools,
|
|
431
|
-
cacheMarkersApplied,
|
|
474
|
+
cacheMarkersApplied: budget.total,
|
|
432
475
|
};
|
|
433
476
|
}
|
|
434
477
|
|
package/src/formatters/native.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
} from './types.js';
|
|
31
31
|
import { normalizeToolPairs, mergeConsecutiveRoles } from './normalize-tool-pairs.js';
|
|
32
32
|
import { isAcceptedImageMediaType, strippedImagePlaceholder } from '../utils/image-media.js';
|
|
33
|
+
import { assertCacheMarkersWithinLimit } from '../utils/cache-marker-budget.js';
|
|
33
34
|
|
|
34
35
|
/** Index of the last content block that can carry cache_control. Anthropic
|
|
35
36
|
* rejects cache_control on thinking / redacted_thinking blocks, so a cache
|
|
@@ -184,6 +185,7 @@ export class NativeFormatter implements PrefillFormatter {
|
|
|
184
185
|
tools,
|
|
185
186
|
systemPrompt,
|
|
186
187
|
promptCaching = false,
|
|
188
|
+
cacheMarkers = 'membrane-system',
|
|
187
189
|
cacheTtl,
|
|
188
190
|
hasCacheMarker,
|
|
189
191
|
contextPrefix,
|
|
@@ -209,7 +211,7 @@ export class NativeFormatter implements PrefillFormatter {
|
|
|
209
211
|
// Add context prefix as first assistant message (for simulacrum seeding)
|
|
210
212
|
if (contextPrefix) {
|
|
211
213
|
const prefixBlock: Record<string, unknown> = { type: 'text', text: contextPrefix };
|
|
212
|
-
if (promptCaching && cacheControl) {
|
|
214
|
+
if (promptCaching && cacheControl && cacheMarkers === 'membrane-system') {
|
|
213
215
|
prefixBlock.cache_control = cacheControl;
|
|
214
216
|
markedBreakpoints++;
|
|
215
217
|
}
|
|
@@ -248,6 +250,13 @@ export class NativeFormatter implements PrefillFormatter {
|
|
|
248
250
|
includeNames: participantMode === 'multiuser' && !isAssistant,
|
|
249
251
|
});
|
|
250
252
|
|
|
253
|
+
if (
|
|
254
|
+
cacheMarkers === 'cm-owned' &&
|
|
255
|
+
content.some((block) => Boolean((block as Record<string, unknown>).cache_control))
|
|
256
|
+
) {
|
|
257
|
+
throw new Error('cm-owned cache markers reject imported block-level cache_control');
|
|
258
|
+
}
|
|
259
|
+
|
|
251
260
|
if (content.length === 0) {
|
|
252
261
|
continue; // Skip empty messages
|
|
253
262
|
}
|
|
@@ -302,7 +311,13 @@ export class NativeFormatter implements PrefillFormatter {
|
|
|
302
311
|
// Build system content. Cache the system block only as a fallback — when no
|
|
303
312
|
// message breakpoint was marked (see note above; otherwise a message
|
|
304
313
|
// breakpoint already caches tools+system as part of its prefix).
|
|
305
|
-
|
|
314
|
+
if (markedBreakpoints > 4) {
|
|
315
|
+
throw new Error(`cache_control limit exceeded: ${markedBreakpoints} markers (maximum 4)`);
|
|
316
|
+
}
|
|
317
|
+
const cacheSystem =
|
|
318
|
+
cacheMarkers === 'membrane-system' && cacheControl && markedBreakpoints === 0
|
|
319
|
+
? cacheControl
|
|
320
|
+
: undefined;
|
|
306
321
|
let systemContent: unknown;
|
|
307
322
|
if (typeof systemPrompt === 'string') {
|
|
308
323
|
if (cacheSystem) {
|
|
@@ -327,6 +342,12 @@ export class NativeFormatter implements PrefillFormatter {
|
|
|
327
342
|
|
|
328
343
|
// Native tools
|
|
329
344
|
const nativeTools = tools?.length ? this.convertToNativeTools(tools) : undefined;
|
|
345
|
+
if (cacheMarkers === 'cm-owned') {
|
|
346
|
+
assertCacheMarkersWithinLimit(
|
|
347
|
+
{ messages: mergedMessages, system: systemContent, tools: nativeTools },
|
|
348
|
+
'native'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
330
351
|
|
|
331
352
|
return {
|
|
332
353
|
messages: mergedMessages,
|
|
@@ -43,6 +43,55 @@ import type { NormalizeEvent } from './types.js';
|
|
|
43
43
|
*/
|
|
44
44
|
export type ProviderBlock = Record<string, unknown> & { type: string };
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Does this repair REWRITE prefix bytes, or only re-shape envelopes?
|
|
48
|
+
*
|
|
49
|
+
* The two `true` entries are placeholders that get rewritten again the moment
|
|
50
|
+
* the real pairing arrives — the synthetic `[pending]` result when its
|
|
51
|
+
* tool_result lands, the textified orphan when its tool_use does — so a cache
|
|
52
|
+
* breakpoint placed at or past one caches a prefix that is about to change,
|
|
53
|
+
* poisoning every subsequent read.
|
|
54
|
+
*
|
|
55
|
+
* EVERY kind is listed, and the type is a `Record` over the whole union
|
|
56
|
+
* precisely so the compiler forces that: a new `NormalizeEvent` kind is a
|
|
57
|
+
* missing-property error here until someone decides which side it falls on.
|
|
58
|
+
* The previous shape (`[...] satisfies Array<NormalizeEvent['kind']>`) only
|
|
59
|
+
* checked that the listed kinds were REAL, and a `satisfies` on an array
|
|
60
|
+
* accepts any subset — so a new prefix-rewriting repair could be added to the
|
|
61
|
+
* union, never listed, and silently escape the cache gate below while the
|
|
62
|
+
* comment claimed a compile error that did not exist.
|
|
63
|
+
*
|
|
64
|
+
* NOTE FOR COMPOSE: the `stray_tool_result_textified` kind landing from the
|
|
65
|
+
* stray-orphan lane belongs here as `true` — it rewrites prefix bytes for the
|
|
66
|
+
* same reason `orphan_tool_result_textified` does.
|
|
67
|
+
*/
|
|
68
|
+
const NORMALIZE_EVENT_REWRITES_PREFIX: Record<NormalizeEvent['kind'], boolean> = {
|
|
69
|
+
block_re_roled: false,
|
|
70
|
+
tool_result_hoisted: false,
|
|
71
|
+
interloper_deferred: false,
|
|
72
|
+
synthetic_pending_result: true,
|
|
73
|
+
orphan_tool_result_textified: true,
|
|
74
|
+
pending_in_flight: false,
|
|
75
|
+
cache_suppressed_for_synthetic: false,
|
|
76
|
+
leading_user_synthesized: false,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function deriveKindsThatRewritePrefix(): ReadonlySet<NormalizeEvent['kind']> {
|
|
80
|
+
const kinds = Object.entries(NORMALIZE_EVENT_REWRITES_PREFIX)
|
|
81
|
+
.filter(([, rewritesPrefix]) => rewritesPrefix)
|
|
82
|
+
.map(([kind]) => kind as NormalizeEvent['kind']);
|
|
83
|
+
return new Set(kinds);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The repairs that rewrite prefix bytes, derived from the exhaustive
|
|
88
|
+
* classification above. Consumers gate cache placement on this SET rather
|
|
89
|
+
* than on a single kind, so a normalizer that grows another prefix-rewriting
|
|
90
|
+
* repair is covered at every cache site by classifying it once.
|
|
91
|
+
*/
|
|
92
|
+
export const PREFIX_REWRITING_NORMALIZE_EVENT_KINDS: ReadonlySet<NormalizeEvent['kind']> =
|
|
93
|
+
deriveKindsThatRewritePrefix();
|
|
94
|
+
|
|
46
95
|
export interface NormalizeOptions {
|
|
47
96
|
/** See `BuildOptions.pendingToolCallIds`. */
|
|
48
97
|
pendingToolCallIds?: ReadonlySet<string>;
|
package/src/formatters/types.ts
CHANGED
|
@@ -49,6 +49,16 @@ export interface BuildOptions {
|
|
|
49
49
|
/** Tool definitions to include */
|
|
50
50
|
tools?: ToolDefinition[];
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The tool mode Membrane RESOLVED for this request (see
|
|
54
|
+
* `Membrane.resolveToolMode`), which already accounts for `request.toolMode`,
|
|
55
|
+
* the formatter's own configured mode, and provider/formatter derivation.
|
|
56
|
+
* A formatter that supports both shapes must build for THIS mode; its
|
|
57
|
+
* constructor-time mode is only the fallback for direct `buildMessages`
|
|
58
|
+
* callers that resolve nothing.
|
|
59
|
+
*/
|
|
60
|
+
toolMode?: 'xml' | 'native';
|
|
61
|
+
|
|
52
62
|
/** Whether thinking is enabled */
|
|
53
63
|
thinking?: { enabled: boolean; budgetTokens?: number };
|
|
54
64
|
|
|
@@ -58,6 +68,9 @@ export interface BuildOptions {
|
|
|
58
68
|
/** Enable prompt caching (Anthropic-specific) */
|
|
59
69
|
promptCaching?: boolean;
|
|
60
70
|
|
|
71
|
+
/** See NormalizedRequest.cacheMarkers. */
|
|
72
|
+
cacheMarkers?: 'membrane-system' | 'cm-owned';
|
|
73
|
+
|
|
61
74
|
/** Cache TTL for Anthropic prompt caching - '5m' (default) or '1h' for extended */
|
|
62
75
|
cacheTtl?: '5m' | '1h';
|
|
63
76
|
|
|
@@ -163,6 +176,18 @@ export interface BuildResult {
|
|
|
163
176
|
/** Number of cache control markers applied (for Anthropic prompt caching) */
|
|
164
177
|
cacheMarkersApplied?: number;
|
|
165
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Offset into the turn's accumulated assistant text at which the CURRENT
|
|
181
|
+
* last message of `messages` begins. Zero (or absent) for an ordinary
|
|
182
|
+
* build: the whole accumulated document is the trailing assistant prefill.
|
|
183
|
+
*
|
|
184
|
+
* A split-turn image injection persists its three messages here and moves
|
|
185
|
+
* this watermark to the image seam, so later continuations replace only
|
|
186
|
+
* the trailing assistant message and never re-flatten the pre-image text
|
|
187
|
+
* over the user turn that carries the image.
|
|
188
|
+
*/
|
|
189
|
+
accumulatedBaseOffset?: number;
|
|
190
|
+
|
|
166
191
|
/**
|
|
167
192
|
* `false` only when the tool-pair normalizer detected a trailing
|
|
168
193
|
* unmatched tool_use whose id is in `pendingToolCallIds` — i.e. the
|
|
@@ -258,6 +283,16 @@ export interface PrefillFormatter {
|
|
|
258
283
|
/** Whether this formatter uses prefill (vs native pass-through) */
|
|
259
284
|
readonly usesPrefill: boolean;
|
|
260
285
|
|
|
286
|
+
/**
|
|
287
|
+
* The tool mode this formatter instance was EXPLICITLY constructed with, if
|
|
288
|
+
* any. Read by `Membrane.resolveToolMode` as the fallback under an explicit
|
|
289
|
+
* `request.toolMode`: a formatter that can build either shape carries its
|
|
290
|
+
* caller's configured choice here so resolution honors it instead of
|
|
291
|
+
* re-deriving one from the formatter's name. Left undefined by formatters
|
|
292
|
+
* that build exactly one shape.
|
|
293
|
+
*/
|
|
294
|
+
readonly configuredToolMode?: 'xml' | 'native';
|
|
295
|
+
|
|
261
296
|
// ==========================================================================
|
|
262
297
|
// REQUEST BUILDING
|
|
263
298
|
// ==========================================================================
|