@animalabs/membrane 0.5.80 → 0.5.81

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 (151) hide show
  1. package/dist/context/index.d.ts +1 -1
  2. package/dist/context/index.d.ts.map +1 -1
  3. package/dist/context/index.js +1 -1
  4. package/dist/context/index.js.map +1 -1
  5. package/dist/context/process.d.ts +3 -1
  6. package/dist/context/process.d.ts.map +1 -1
  7. package/dist/context/process.js +230 -49
  8. package/dist/context/process.js.map +1 -1
  9. package/dist/context/types.d.ts +30 -0
  10. package/dist/context/types.d.ts.map +1 -1
  11. package/dist/context/types.js +16 -0
  12. package/dist/context/types.js.map +1 -1
  13. package/dist/floating-cache-marker.test.js +21 -0
  14. package/dist/floating-cache-marker.test.js.map +1 -1
  15. package/dist/formatters/anthropic-xml.d.ts +2 -0
  16. package/dist/formatters/anthropic-xml.d.ts.map +1 -1
  17. package/dist/formatters/anthropic-xml.js +57 -19
  18. package/dist/formatters/anthropic-xml.js.map +1 -1
  19. package/dist/formatters/normalize-tool-pairs.d.ts +7 -0
  20. package/dist/formatters/normalize-tool-pairs.d.ts.map +1 -1
  21. package/dist/formatters/normalize-tool-pairs.js +45 -0
  22. package/dist/formatters/normalize-tool-pairs.js.map +1 -1
  23. package/dist/formatters/types.d.ts +29 -0
  24. package/dist/formatters/types.d.ts.map +1 -1
  25. package/dist/membrane.d.ts +137 -8
  26. package/dist/membrane.d.ts.map +1 -1
  27. package/dist/membrane.js +812 -310
  28. package/dist/membrane.js.map +1 -1
  29. package/dist/providers/anthropic.d.ts +26 -0
  30. package/dist/providers/anthropic.d.ts.map +1 -1
  31. package/dist/providers/anthropic.js +142 -25
  32. package/dist/providers/anthropic.js.map +1 -1
  33. package/dist/providers/bedrock.d.ts +8 -0
  34. package/dist/providers/bedrock.d.ts.map +1 -1
  35. package/dist/providers/bedrock.js +24 -1
  36. package/dist/providers/bedrock.js.map +1 -1
  37. package/dist/providers/gemini.d.ts +11 -0
  38. package/dist/providers/gemini.d.ts.map +1 -1
  39. package/dist/providers/gemini.js +104 -82
  40. package/dist/providers/gemini.js.map +1 -1
  41. package/dist/providers/mock.d.ts +2 -0
  42. package/dist/providers/mock.d.ts.map +1 -1
  43. package/dist/providers/mock.js +2 -0
  44. package/dist/providers/mock.js.map +1 -1
  45. package/dist/providers/openai-compatible.d.ts +7 -0
  46. package/dist/providers/openai-compatible.d.ts.map +1 -1
  47. package/dist/providers/openai-compatible.js +99 -48
  48. package/dist/providers/openai-compatible.js.map +1 -1
  49. package/dist/providers/openai-completions.d.ts +5 -0
  50. package/dist/providers/openai-completions.d.ts.map +1 -1
  51. package/dist/providers/openai-completions.js +107 -44
  52. package/dist/providers/openai-completions.js.map +1 -1
  53. package/dist/providers/openai-responses-api.d.ts +16 -0
  54. package/dist/providers/openai-responses-api.d.ts.map +1 -1
  55. package/dist/providers/openai-responses-api.js +38 -10
  56. package/dist/providers/openai-responses-api.js.map +1 -1
  57. package/dist/providers/openai-responses.d.ts +2 -0
  58. package/dist/providers/openai-responses.d.ts.map +1 -1
  59. package/dist/providers/openai-responses.js +8 -1
  60. package/dist/providers/openai-responses.js.map +1 -1
  61. package/dist/providers/openai.d.ts +7 -0
  62. package/dist/providers/openai.d.ts.map +1 -1
  63. package/dist/providers/openai.js +92 -44
  64. package/dist/providers/openai.js.map +1 -1
  65. package/dist/providers/openrouter.d.ts +9 -0
  66. package/dist/providers/openrouter.d.ts.map +1 -1
  67. package/dist/providers/openrouter.js +118 -62
  68. package/dist/providers/openrouter.js.map +1 -1
  69. package/dist/providers/utils.d.ts +74 -2
  70. package/dist/providers/utils.d.ts.map +1 -1
  71. package/dist/providers/utils.js +184 -3
  72. package/dist/providers/utils.js.map +1 -1
  73. package/dist/registry/default-pricing.d.ts +22 -0
  74. package/dist/registry/default-pricing.d.ts.map +1 -1
  75. package/dist/registry/default-pricing.js +162 -28
  76. package/dist/registry/default-pricing.js.map +1 -1
  77. package/dist/types/content.d.ts +13 -0
  78. package/dist/types/content.d.ts.map +1 -1
  79. package/dist/types/content.js.map +1 -1
  80. package/dist/types/errors.d.ts +15 -0
  81. package/dist/types/errors.d.ts.map +1 -1
  82. package/dist/types/errors.js +26 -0
  83. package/dist/types/errors.js.map +1 -1
  84. package/dist/types/index.d.ts +3 -3
  85. package/dist/types/index.d.ts.map +1 -1
  86. package/dist/types/index.js +1 -1
  87. package/dist/types/index.js.map +1 -1
  88. package/dist/types/provider.d.ts +50 -0
  89. package/dist/types/provider.d.ts.map +1 -1
  90. package/dist/types/response.d.ts +85 -4
  91. package/dist/types/response.d.ts.map +1 -1
  92. package/dist/types/response.js.map +1 -1
  93. package/dist/types/streaming.d.ts +6 -1
  94. package/dist/types/streaming.d.ts.map +1 -1
  95. package/dist/utils/cache-marker-budget.d.ts +82 -0
  96. package/dist/utils/cache-marker-budget.d.ts.map +1 -0
  97. package/dist/utils/cache-marker-budget.js +189 -0
  98. package/dist/utils/cache-marker-budget.js.map +1 -0
  99. package/dist/utils/cost.d.ts +22 -0
  100. package/dist/utils/cost.d.ts.map +1 -1
  101. package/dist/utils/cost.js +55 -0
  102. package/dist/utils/cost.js.map +1 -1
  103. package/dist/utils/thinking-carriers.d.ts +80 -0
  104. package/dist/utils/thinking-carriers.d.ts.map +1 -0
  105. package/dist/utils/thinking-carriers.js +115 -0
  106. package/dist/utils/thinking-carriers.js.map +1 -0
  107. package/dist/utils/tool-parser.d.ts +24 -0
  108. package/dist/utils/tool-parser.d.ts.map +1 -1
  109. package/dist/utils/tool-parser.js +367 -161
  110. package/dist/utils/tool-parser.js.map +1 -1
  111. package/dist/utils/usage.d.ts +71 -0
  112. package/dist/utils/usage.d.ts.map +1 -0
  113. package/dist/utils/usage.js +161 -0
  114. package/dist/utils/usage.js.map +1 -0
  115. package/dist/yielding-stream.d.ts +8 -0
  116. package/dist/yielding-stream.d.ts.map +1 -1
  117. package/dist/yielding-stream.js +49 -3
  118. package/dist/yielding-stream.js.map +1 -1
  119. package/package.json +1 -1
  120. package/src/context/index.ts +1 -0
  121. package/src/context/process.ts +305 -59
  122. package/src/context/types.ts +38 -0
  123. package/src/floating-cache-marker.test.ts +24 -0
  124. package/src/formatters/anthropic-xml.ts +62 -20
  125. package/src/formatters/normalize-tool-pairs.ts +49 -0
  126. package/src/formatters/types.ts +32 -0
  127. package/src/membrane.ts +963 -313
  128. package/src/providers/anthropic.ts +161 -19
  129. package/src/providers/bedrock.ts +25 -1
  130. package/src/providers/gemini.ts +125 -87
  131. package/src/providers/mock.ts +3 -0
  132. package/src/providers/openai-compatible.ts +102 -45
  133. package/src/providers/openai-completions.ts +112 -46
  134. package/src/providers/openai-responses-api.ts +45 -15
  135. package/src/providers/openai-responses.ts +8 -1
  136. package/src/providers/openai.ts +95 -42
  137. package/src/providers/openrouter.ts +125 -63
  138. package/src/providers/utils.ts +227 -6
  139. package/src/registry/default-pricing.ts +165 -28
  140. package/src/types/content.ts +14 -0
  141. package/src/types/errors.ts +28 -0
  142. package/src/types/index.ts +6 -0
  143. package/src/types/provider.ts +55 -0
  144. package/src/types/response.ts +109 -8
  145. package/src/types/streaming.ts +8 -2
  146. package/src/utils/cache-marker-budget.ts +208 -0
  147. package/src/utils/cost.ts +64 -0
  148. package/src/utils/thinking-carriers.ts +125 -0
  149. package/src/utils/tool-parser.ts +473 -174
  150. package/src/utils/usage.ts +184 -0
  151. package/src/yielding-stream.ts +54 -3
@@ -0,0 +1,184 @@
1
+ import type { ModelPricing, ProviderResponse, UsageCacheConvention } from '../types/provider.js';
2
+ import type { CostBreakdown, DetailedUsage, TurnRoundUsage } from '../types/response.js';
3
+ import { addCostBreakdowns, calculateCost, warnUnpricedModel } from './cost.js';
4
+
5
+ /** Adapters whose convention is undeclared warn once each, not once per call. */
6
+ const warnedUndeclaredAdapters = new Set<string>();
7
+
8
+ /**
9
+ * Restate a provider's usage in membrane's single convention: `inputTokens`,
10
+ * `cacheReadTokens` and `cacheCreationTokens` DISJOINT, each priced at its own
11
+ * rate. Adapters reporting `cache-inclusive` counts have the cached span
12
+ * subtracted out of `inputTokens`; the total prompt size stays recoverable as
13
+ * `inputTokens + cacheReadTokens`.
14
+ *
15
+ * Without this, one field name carried two incomparable meanings — the cache
16
+ * hit ratio was unbounded on cache-excluded adapters and cost double-charged
17
+ * the cached span on cache-inclusive ones.
18
+ */
19
+ export function normalizeUsageToCacheExcluded(
20
+ usage: ProviderResponse['usage'],
21
+ adapterName: string,
22
+ adapterConvention: UsageCacheConvention | undefined,
23
+ ): ProviderResponse['usage'] {
24
+ // An adapter that declares nothing is in exactly the state `unknown` names,
25
+ // which is why the interface field is optional and lands here.
26
+ const convention = usage.cacheConvention ?? adapterConvention ?? 'unknown';
27
+ const cacheReadTokens = usage.cacheReadTokens ?? 0;
28
+
29
+ if (convention === 'unknown' && cacheReadTokens > 0 && !warnedUndeclaredAdapters.has(adapterName)) {
30
+ warnedUndeclaredAdapters.add(adapterName);
31
+ console.warn(
32
+ `[membrane:usage] adapter "${adapterName}" reported ${cacheReadTokens} cache-read tokens but`
33
+ + ' declares usageCacheConvention "unknown", so membrane cannot tell whether inputTokens'
34
+ + ` (${usage.inputTokens}) already includes them. Counts are passed through unchanged;`
35
+ + ' cost and cache hit ratio may be off by the cached span until the adapter declares'
36
+ + ' "cache-excluded" or "cache-inclusive".'
37
+ );
38
+ }
39
+
40
+ if (convention !== 'cache-inclusive' || cacheReadTokens === 0) return usage;
41
+
42
+ return { ...usage, inputTokens: Math.max(0, usage.inputTokens - cacheReadTokens) };
43
+ }
44
+
45
+ /**
46
+ * Share of the prompt that was served from cache. Both terms are in the
47
+ * cache-excluded convention, so this is a true ratio in [0, 1] regardless of
48
+ * which provider produced the counts.
49
+ */
50
+ export function calculateCacheHitRatio(
51
+ usage: { inputTokens: number; cacheReadTokens?: number },
52
+ ): number {
53
+ const cacheReadTokens = usage.cacheReadTokens ?? 0;
54
+ const promptTokens = (usage.inputTokens ?? 0) + cacheReadTokens;
55
+ if (promptTokens === 0) return 0;
56
+ return cacheReadTokens / promptTokens;
57
+ }
58
+
59
+ /** Test seam: the once-per-adapter warn latch is process-wide otherwise. */
60
+ export function resetUndeclaredConventionWarnings(): void {
61
+ warnedUndeclaredAdapters.clear();
62
+ }
63
+
64
+ /** Unconvertible provider item types warn once each, not once per response. */
65
+ const warnedUnconvertibleProviderItems = new Set<string>();
66
+
67
+ /**
68
+ * A provider content item membrane cannot normalize is preserved verbatim on a
69
+ * zero-width carrier rather than dropped, but the caller should know its
70
+ * content is invisible to normalized consumers. Once per type, not per item.
71
+ */
72
+ export function warnUnconvertibleProviderItem(itemType: string): void {
73
+ if (warnedUnconvertibleProviderItems.has(itemType)) return;
74
+ warnedUnconvertibleProviderItems.add(itemType);
75
+ console.warn(
76
+ `[membrane:content] no normalized ContentBlock for provider item type "${itemType}"`
77
+ + ' — preserving it verbatim as a rawItem carrier so it can be replayed, but its'
78
+ + ' content is not visible to normalized consumers.'
79
+ );
80
+ }
81
+
82
+ /** Test seam: the once-per-type warn latch is process-wide otherwise. */
83
+ export function resetUnconvertibleProviderItemWarnings(): void {
84
+ warnedUnconvertibleProviderItems.clear();
85
+ }
86
+
87
+ /**
88
+ * Accumulates one turn's usage ACROSS ROUNDS, pricing each round under the
89
+ * model that actually served it.
90
+ *
91
+ * A routed turn is not billed at one rate: OpenRouter re-picks a provider per
92
+ * call and an alias can resolve to a new snapshot mid-turn. Every streaming
93
+ * loop used to re-resolve pricing when the served model changed and then
94
+ * recompute the WHOLE accumulated usage at that latest rate, retroactively
95
+ * re-billing every earlier round — two 1M-token rounds at 1,000/M then
96
+ * 10,000/M reported 20,000 against a real 11,000.
97
+ *
98
+ * The four tool loops (callback/yielding × XML/native) each carried their own
99
+ * copy of the accumulate-and-price block; this is the single one they share.
100
+ */
101
+ export class TurnUsageAccumulator {
102
+ private readonly rounds: TurnRoundUsage[] = [];
103
+ private readonly tokens: DetailedUsage = { inputTokens: 0, outputTokens: 0 };
104
+ private summedCost: CostBreakdown | undefined;
105
+ /** False once any round could not be priced, or two rounds disagreed on currency. */
106
+ private costCoversEveryRound = true;
107
+ private lastServed: string | undefined;
108
+
109
+ constructor(
110
+ private readonly requestedModel: string,
111
+ private readonly resolveRoundPricing: (servedModel?: string) => ModelPricing | undefined,
112
+ ) {}
113
+
114
+ /**
115
+ * Fold one provider round in, priced at its own served model, and return the
116
+ * turn total as it now stands (a fresh snapshot — callers hand this to
117
+ * `onUsage` and to stream events, and must not see it mutate underneath).
118
+ */
119
+ addRound(servedModel: string | undefined, usage: ProviderResponse['usage']): DetailedUsage {
120
+ if (servedModel) this.lastServed = servedModel;
121
+
122
+ this.tokens.inputTokens += usage.inputTokens;
123
+ this.tokens.outputTokens += usage.outputTokens;
124
+ if (usage.cacheCreationTokens) {
125
+ this.tokens.cacheCreationTokens = (this.tokens.cacheCreationTokens ?? 0) + usage.cacheCreationTokens;
126
+ }
127
+ if (usage.cacheReadTokens) {
128
+ this.tokens.cacheReadTokens = (this.tokens.cacheReadTokens ?? 0) + usage.cacheReadTokens;
129
+ }
130
+ // Already inside outputTokens (billed at the output rate) — summed for
131
+ // attribution, never added to the output total.
132
+ if (usage.thinkingTokens) {
133
+ this.tokens.thinkingTokens = (this.tokens.thinkingTokens ?? 0) + usage.thinkingTokens;
134
+ }
135
+
136
+ const roundModel = servedModel || this.requestedModel;
137
+ const roundPricing = this.resolveRoundPricing(servedModel);
138
+ const roundCost = roundPricing ? calculateCost(usage, roundPricing) : undefined;
139
+ if (!roundPricing) warnUnpricedModel(roundModel);
140
+
141
+ this.rounds.push({
142
+ model: roundModel,
143
+ usage: {
144
+ inputTokens: usage.inputTokens,
145
+ outputTokens: usage.outputTokens,
146
+ ...(usage.cacheCreationTokens != null ? { cacheCreationTokens: usage.cacheCreationTokens } : {}),
147
+ ...(usage.cacheReadTokens != null ? { cacheReadTokens: usage.cacheReadTokens } : {}),
148
+ ...(usage.thinkingTokens != null ? { thinkingTokens: usage.thinkingTokens } : {}),
149
+ ...(roundCost ? { estimatedCost: roundCost } : {}),
150
+ },
151
+ });
152
+
153
+ // A partial sum presented as the turn total is a false number, and an
154
+ // absent cost already means "membrane does not know". So one unpriced (or
155
+ // unsummable) round drops the total rather than under-reporting it; the
156
+ // rounds that WERE priced stay readable in the roster.
157
+ if (!roundCost) {
158
+ this.costCoversEveryRound = false;
159
+ } else if (this.costCoversEveryRound) {
160
+ this.summedCost = this.summedCost ? addCostBreakdowns(this.summedCost, roundCost) : roundCost;
161
+ if (!this.summedCost) this.costCoversEveryRound = false;
162
+ }
163
+
164
+ return this.total;
165
+ }
166
+
167
+ /** Turn totals: summed tokens, plus the summed per-round cost when every round had rates. */
168
+ get total(): DetailedUsage {
169
+ return {
170
+ ...this.tokens,
171
+ ...(this.costCoversEveryRound && this.summedCost ? { estimatedCost: this.summedCost } : {}),
172
+ };
173
+ }
174
+
175
+ /** The model that served the LAST round, or undefined if no round named one. */
176
+ get lastServedModel(): string | undefined {
177
+ return this.lastServed;
178
+ }
179
+
180
+ /** One entry per provider round, in order — the audit trail behind the summed total. */
181
+ get perRound(): TurnRoundUsage[] {
182
+ return this.rounds.map((round) => ({ model: round.model, usage: { ...round.usage } }));
183
+ }
184
+ }
@@ -70,6 +70,18 @@ export class YieldingStreamImpl implements YieldingStream {
70
70
  // Flag indicating the stream producer is done
71
71
  private producerDone = false;
72
72
 
73
+ // Set when the CONSUMER leaves — `for await … break` (iterator.return) or
74
+ // iterator.throw. Nobody will read this stream again, so queued events are
75
+ // dropped rather than accumulated for a reader that is never coming back.
76
+ private consumerDeparted = false;
77
+
78
+ // Held so the listener can be removed at terminal: a long-lived caller
79
+ // signal shared across many streams otherwise accumulates one closure per
80
+ // stream, none of which are ever released.
81
+ private readonly onExternalAbort = () => {
82
+ this.cancel();
83
+ };
84
+
73
85
  constructor(
74
86
  private readonly options: YieldingStreamOptions,
75
87
  private readonly runInference: (stream: YieldingStreamImpl) => Promise<void>
@@ -78,9 +90,7 @@ export class YieldingStreamImpl implements YieldingStream {
78
90
 
79
91
  // Link external signal if provided
80
92
  if (options.signal) {
81
- options.signal.addEventListener('abort', () => {
82
- this.cancel();
83
- });
93
+ options.signal.addEventListener('abort', this.onExternalAbort, { once: true });
84
94
  }
85
95
  }
86
96
 
@@ -179,6 +189,7 @@ export class YieldingStreamImpl implements YieldingStream {
179
189
  this.emit({ type: 'aborted', reason: 'user' });
180
190
  this.producerDone = true;
181
191
  this.state = { status: 'done' };
192
+ this.releaseExternalSignal();
182
193
  }
183
194
 
184
195
  // ============================================================================
@@ -215,6 +226,27 @@ export class YieldingStreamImpl implements YieldingStream {
215
226
  await this.waitForEvent();
216
227
  }
217
228
  },
229
+
230
+ // `for await … break`, `return` out of the loop body, and a throw in
231
+ // the loop body all call return(). Without it the consumer walks away
232
+ // and the producer keeps streaming, resuming and billing. Cancelling
233
+ // here is the same act the consumer would perform by hand.
234
+ return: async (): Promise<IteratorResult<StreamEvent>> => {
235
+ this.departConsumer();
236
+ return { value: undefined as unknown as StreamEvent, done: true };
237
+ },
238
+
239
+ // Reached through `yield*` delegation, or by a caller injecting an
240
+ // error by hand. Same departure as return() — nobody is reading this
241
+ // stream again — and then the error continues on its way, exactly as an
242
+ // async generator with no handler of its own would rethrow it. Reporting
243
+ // `done: true` here instead swallowed it: a `yield*` delegating to this
244
+ // stream resumed after the delegation as if nothing had been thrown in,
245
+ // and a direct `.throw(e)` resolved rather than rejecting.
246
+ throw: async (error?: unknown): Promise<IteratorResult<StreamEvent>> => {
247
+ this.departConsumer();
248
+ throw error;
249
+ },
218
250
  };
219
251
  }
220
252
 
@@ -226,6 +258,9 @@ export class YieldingStreamImpl implements YieldingStream {
226
258
  * Push an event to be yielded to the consumer.
227
259
  */
228
260
  emit(event: StreamEvent): void {
261
+ // The consumer has left for good; queueing here would grow without bound
262
+ // for a reader that cannot observe it.
263
+ if (this.consumerDeparted) return;
229
264
  this.eventQueue.push(event);
230
265
  this.notifyEventWaiter();
231
266
  }
@@ -255,6 +290,7 @@ export class YieldingStreamImpl implements YieldingStream {
255
290
  */
256
291
  markDone(): void {
257
292
  this.producerDone = true;
293
+ this.releaseExternalSignal();
258
294
  this.notifyEventWaiter();
259
295
  }
260
296
 
@@ -269,6 +305,21 @@ export class YieldingStreamImpl implements YieldingStream {
269
305
  // Private Helpers
270
306
  // ============================================================================
271
307
 
308
+ /**
309
+ * The consumer has stopped iterating for good: drop everything queued for
310
+ * it, refuse further queueing, and cancel the producer.
311
+ */
312
+ private departConsumer(): void {
313
+ this.consumerDeparted = true;
314
+ this.eventQueue.length = 0;
315
+ this.cancel();
316
+ this.eventQueue.length = 0;
317
+ }
318
+
319
+ private releaseExternalSignal(): void {
320
+ this.options.signal?.removeEventListener('abort', this.onExternalAbort);
321
+ }
322
+
272
323
  private startInference(): void {
273
324
  if (this.state.status !== 'idle') {
274
325
  return; // Already started