@animalabs/membrane 0.5.54 → 0.5.63

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 (40) hide show
  1. package/dist/formatters/native.d.ts.map +1 -1
  2. package/dist/formatters/native.js +11 -0
  3. package/dist/formatters/native.js.map +1 -1
  4. package/dist/formatters/normalize-tool-pairs.d.ts +4 -2
  5. package/dist/formatters/normalize-tool-pairs.d.ts.map +1 -1
  6. package/dist/formatters/normalize-tool-pairs.js +95 -22
  7. package/dist/formatters/normalize-tool-pairs.js.map +1 -1
  8. package/dist/formatters/types.d.ts +26 -0
  9. package/dist/formatters/types.d.ts.map +1 -1
  10. package/dist/membrane.d.ts +10 -0
  11. package/dist/membrane.d.ts.map +1 -1
  12. package/dist/membrane.js +118 -13
  13. package/dist/membrane.js.map +1 -1
  14. package/dist/providers/anthropic.d.ts.map +1 -1
  15. package/dist/providers/anthropic.js +83 -2
  16. package/dist/providers/anthropic.js.map +1 -1
  17. package/dist/providers/openai-compatible.d.ts.map +1 -1
  18. package/dist/providers/openai-compatible.js +3 -0
  19. package/dist/providers/openai-compatible.js.map +1 -1
  20. package/dist/providers/openai-completions.d.ts.map +1 -1
  21. package/dist/providers/openai-completions.js +57 -3
  22. package/dist/providers/openai-completions.js.map +1 -1
  23. package/dist/providers/openai.d.ts.map +1 -1
  24. package/dist/providers/openai.js +3 -0
  25. package/dist/providers/openai.js.map +1 -1
  26. package/dist/types/provider.d.ts +9 -0
  27. package/dist/types/provider.d.ts.map +1 -1
  28. package/dist/types/request.d.ts +10 -0
  29. package/dist/types/request.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/formatters/native.ts +10 -0
  32. package/src/formatters/normalize-tool-pairs.ts +100 -25
  33. package/src/formatters/types.ts +28 -1
  34. package/src/membrane.ts +129 -13
  35. package/src/providers/anthropic.ts +87 -3
  36. package/src/providers/openai-compatible.ts +4 -0
  37. package/src/providers/openai-completions.ts +58 -2
  38. package/src/providers/openai.ts +4 -0
  39. package/src/types/provider.ts +10 -0
  40. package/src/types/request.ts +12 -1
@@ -126,6 +126,8 @@ export interface ProviderRequest {
126
126
  presencePenalty?: number;
127
127
  /** Frequency penalty */
128
128
  frequencyPenalty?: number;
129
+ /** Repetition penalty (multiplicative, vLLM/HuggingFace style) */
130
+ repetitionPenalty?: number;
129
131
  /** Stop sequences */
130
132
  stopSequences?: string[];
131
133
  /** Tools in provider format */
@@ -140,6 +142,13 @@ export interface ProviderRequestOptions {
140
142
  idleTimeoutMs?: number;
141
143
  /** Called with the raw API request body right before fetch */
142
144
  onRequest?: (rawRequest: unknown) => void;
145
+ /**
146
+ * Wrap native thinking deltas in <thinking>...</thinking> tags on the
147
+ * onChunk stream. Used by the XML formatter path so its tag-based parser
148
+ * tracks thinking blocks; without this, native thinking content streams
149
+ * indistinguishably from visible text.
150
+ */
151
+ wrapThinkingTags?: boolean;
143
152
  }
144
153
  export interface ProviderResponse {
145
154
  /** Raw response content */
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/types/provider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/types/provider.ts"],"names":[],"mappings":"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"}
@@ -18,10 +18,20 @@ export interface GenerationConfig {
18
18
  presencePenalty?: number;
19
19
  /** Frequency penalty (provider-specific) */
20
20
  frequencyPenalty?: number;
21
+ /** Repetition penalty — multiplicative (vLLM/HuggingFace style, typically 1.0-1.2) */
22
+ repetitionPenalty?: number;
21
23
  /** Enable thinking/reasoning mode */
22
24
  thinking?: {
23
25
  enabled: boolean;
24
26
  budgetTokens?: number;
27
+ /** Thinking type for the API: 'enabled' (default, explicit budget) or 'adaptive' (model-managed) */
28
+ type?: 'enabled' | 'adaptive';
29
+ /**
30
+ * Controls how thinking content is returned: 'summarized' (readable summary)
31
+ * or 'omitted' (empty thinking field, signature only). Models like Fable 5 /
32
+ * Opus 4.7+ default to 'omitted' — set 'summarized' to receive thinking text.
33
+ */
34
+ display?: 'summarized' | 'omitted';
25
35
  };
26
36
  /** Image generation config (Gemini) */
27
37
  imageGeneration?: {
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/types/request.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/types/request.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/membrane",
3
- "version": "0.5.54",
3
+ "version": "0.5.63",
4
4
  "description": "LLM middleware - a selective boundary that transforms what passes through",
5
5
  "repository": {
6
6
  "type": "git",
@@ -385,10 +385,20 @@ export class NativeFormatter implements PrefillFormatter {
385
385
  is_error: block.isError,
386
386
  });
387
387
  } else if (block.type === 'thinking') {
388
+ // Round-trip thinking blocks verbatim, including the signature — the
389
+ // API validates it and (on display:'omitted' models) decrypts it to
390
+ // reconstruct the original reasoning. Signature-only blocks (empty
391
+ // thinking field) are valid and must be passed back unchanged.
388
392
  result.push({
389
393
  type: 'thinking',
390
394
  thinking: block.thinking,
395
+ ...((block as { signature?: string }).signature
396
+ ? { signature: (block as { signature?: string }).signature }
397
+ : {}),
391
398
  });
399
+ } else if (block.type === 'redacted_thinking') {
400
+ // Pass through verbatim (carries encrypted data field)
401
+ result.push({ ...(block as unknown as Record<string, unknown>) });
392
402
  } else if (block.type === 'document' || block.type === 'audio') {
393
403
  hasUnsupportedMedia = true;
394
404
  }
@@ -16,11 +16,13 @@
16
16
  * output is shipped, so producer-side bugs cannot leak the same 400 family
17
17
  * (compression-bug 5/6/7/8/9, agent-framework #37, 2026-05-22 miner stall).
18
18
  *
19
- * Algorithm overview (six phases): reclassify blocks by required role,
19
+ * Algorithm overview (eight phases): reclassify blocks by required role,
20
20
  * reflow into role-correct envelopes, hoist matching tool_results across
21
21
  * the assistant→user boundary, evict interlopers wedged between use and
22
22
  * result, synthesize `[pending]` results for trailing orphans (or signal
23
- * not-ready when the id is in the caller-supplied pending set), validate.
23
+ * not-ready when the id is in the caller-supplied pending set), drop
24
+ * empty envelopes, prepend a synthetic `[continuing]` user envelope when
25
+ * the first envelope ended up assistant-role, validate.
24
26
  */
25
27
 
26
28
  import type { ProviderMessage as LooseProviderMessage } from './types.js';
@@ -123,36 +125,108 @@ export function normalizeToolPairs(
123
125
  // Phase 5.5: suppress cache_control on/after any envelope containing
124
126
  // a synthetic block, so cache keys don't get invalidated when the
125
127
  // real result arrives in a later round.
128
+ //
129
+ // The suppression itself happens here (we know which envelope is the
130
+ // first synthetic by its position in the current array), but the
131
+ // `cache_suppressed_for_synthetic` telemetry is deferred until after
132
+ // phase 6 (which can drop empty envelopes before the synthetic) and
133
+ // phase 7 (which can prepend a `[continuing]` envelope, shifting
134
+ // everything by +1). The event's `envelopeIndex` must refer to the
135
+ // final output array so consumers can index back into it reliably.
136
+ // We pin the envelope by reference and recompute the index after
137
+ // those phases settle.
126
138
  // ---------------------------------------------------------------------
139
+ let pendingCacheSuppressionRef: Envelope | null = null;
127
140
  if (orphanRes.firstSyntheticEnvelope !== null) {
128
- suppressCacheControlFrom(envelopes, orphanRes.firstSyntheticEnvelope, onEvent);
141
+ const ref = envelopes[orphanRes.firstSyntheticEnvelope]!;
142
+ const suppressed = suppressCacheControlFrom(envelopes, orphanRes.firstSyntheticEnvelope);
143
+ if (suppressed) {
144
+ pendingCacheSuppressionRef = ref;
145
+ }
129
146
  }
130
147
 
131
148
  // ---------------------------------------------------------------------
132
149
  // Phase 6: drop empty envelopes (can arise from phase 4 dropping or
133
- // phase 3 hoisting), repair first-message-must-be-user, validate. We
134
- // deliberately do NOT merge consecutive same-role envelopes here —
135
- // that's the formatter's job.
150
+ // phase 3 hoisting). We deliberately do NOT merge consecutive
151
+ // same-role envelopes here — that's the formatter's job.
152
+ //
153
+ // The synthetic-bearing envelope (held by `pendingCacheSuppressionRef`)
154
+ // cannot be dropped here — phase 5 unshifts its synthetic block onto
155
+ // that envelope's content, so it's guaranteed non-empty.
136
156
  // ---------------------------------------------------------------------
137
157
  envelopes = envelopes.filter((e) => e.content.length > 0);
138
158
 
139
- // First-message-must-be-user repair: only repair the case where the
140
- // original input's first message WAS user, but re-roling moved blocks
141
- // to a leading assistant envelope (e.g. misplaced thinking block).
142
- // If the producer genuinely shipped an assistant-first conversation,
143
- // that's a real bug and validate() will throw.
144
- const originalFirstRole = input.length > 0 ? input[0]!.role : 'user';
145
- if (
146
- envelopes.length > 0 &&
147
- envelopes[0]!.role === 'assistant' &&
148
- originalFirstRole === 'user'
149
- ) {
159
+ // ---------------------------------------------------------------------
160
+ // Phase 7: ensure first envelope is user-role.
161
+ //
162
+ // Anthropic requires `messages[0].role === 'user'`. The leading
163
+ // envelope can become assistant for two distinct reasons:
164
+ //
165
+ // (a) Re-roling artifact — a strict-role block (thinking, tool_use)
166
+ // lived under a user-role input message and phase 1+2 moved it
167
+ // to a new leading assistant envelope. `originalFirstRole`
168
+ // is `'user'`.
169
+ //
170
+ // (b) Producer bug — a context strategy genuinely selected an
171
+ // assistant message as the first message of its compiled view
172
+ // (the 2026-05-26 reviewer postmortem: PassthroughStrategy
173
+ // `selectFromEnd` cut on an assistant turn). `originalFirstRole`
174
+ // is `'assistant'`.
175
+ //
176
+ // Both cases get the same repair (prepend a `[continuing]` user
177
+ // envelope) because deletion would lose content in case (a) — the
178
+ // re-roled blocks are real conversation content the producer
179
+ // expected to ship. The synthetic costs a leading cache miss
180
+ // (deterministic literal, so idempotent across identical inputs)
181
+ // but preserves API correctness and producer simplicity. We emit
182
+ // a warn-level event so telemetry can distinguish the causes and
183
+ // alert on (b) without coupling control flow to attribution.
184
+ //
185
+ // Idempotency: the synthetic content is a fixed literal. Running
186
+ // normalize twice on the same input produces identical output the
187
+ // second time (envelope[0] is user, gate doesn't fire).
188
+ // ---------------------------------------------------------------------
189
+ if (envelopes.length > 0 && envelopes[0]!.role === 'assistant') {
190
+ // `input` is guaranteed non-empty here: rebuildEnvelopes only
191
+ // produces envelopes when iterating input messages, so a non-empty
192
+ // envelopes implies a non-empty input.
193
+ const originalFirstRole = input[0]!.role;
194
+ const leadingBlockTypes = envelopes[0]!.content.map((b) => b.type);
150
195
  envelopes.unshift({ role: 'user', content: [{ type: 'text', text: '[continuing]' }] });
196
+ onEvent({ kind: 'leading_user_synthesized', originalFirstRole, leadingBlockTypes });
197
+ }
198
+
199
+ // ---------------------------------------------------------------------
200
+ // Deferred phase 5.5 telemetry: emit `cache_suppressed_for_synthetic`
201
+ // now that index-mutating phases (6, 7) have settled. The envelope
202
+ // reference pinned in phase 5.5 survives both — phase 6 can't drop it
203
+ // (the synthetic block keeps it non-empty), and phase 7 either leaves
204
+ // it in place or shifts it by +1 via unshift.
205
+ // ---------------------------------------------------------------------
206
+ if (pendingCacheSuppressionRef !== null) {
207
+ const envelopeIndex = envelopes.indexOf(pendingCacheSuppressionRef);
208
+ // Assertion: indexOf must succeed. Phases 6 and 7 only filter/prepend;
209
+ // neither can remove an envelope holding a synthetic block.
210
+ if (envelopeIndex < 0) {
211
+ throw new MembraneNormalizerError(
212
+ `Phase 5.5 envelope reference vanished between phase 6 and phase 7 — ` +
213
+ `internal bug: synthetic-bearing envelope should be reachable after both phases.`,
214
+ input.map(cloneMsg),
215
+ envelopes.map(toProviderMessage),
216
+ );
217
+ }
218
+ onEvent({ kind: 'cache_suppressed_for_synthetic', envelopeIndex });
151
219
  }
152
220
 
153
- // Validate. When `ready === false` we intentionally have unmatched
154
- // tool_uses but ONLY the ones in `pending` are allowed to remain
155
- // unsynthesized. Any other gap is a bug in phase 5 and must throw.
221
+ // ---------------------------------------------------------------------
222
+ // Phase 8: validate. When `ready === false` we intentionally have
223
+ // unmatched tool_uses but ONLY the ones in `pending` are allowed to
224
+ // remain unsynthesized. Any other gap is a bug in phase 5 and must
225
+ // throw. The first-message-must-be-user branch should be unreachable
226
+ // after phase 7; it remains as defense-in-depth against a future
227
+ // phase introducing a leading assistant envelope without firing
228
+ // phase 7.
229
+ // ---------------------------------------------------------------------
156
230
  validate(envelopes, input, pending);
157
231
 
158
232
  return { messages: envelopes.map(toProviderMessage), ready };
@@ -459,8 +533,7 @@ function resolveOrphans(
459
533
  function suppressCacheControlFrom(
460
534
  envelopes: Envelope[],
461
535
  startIndex: number,
462
- onEvent: (e: NormalizeEvent) => void,
463
- ): void {
536
+ ): boolean {
464
537
  // Strip cache_control from blocks at-or-after startIndex. We must NOT
465
538
  // mutate the caller's input blocks (envelopes share references with
466
539
  // the input via rebuildEnvelopes), so clone-on-write: replace any
@@ -468,6 +541,10 @@ function suppressCacheControlFrom(
468
541
  // The envelope's content array is replaced wholesale via .map; this
469
542
  // is the only place in the normalizer that creates new block objects
470
543
  // out of existing ones (synthetics aside).
544
+ //
545
+ // Returns whether any block was actually suppressed, so the caller
546
+ // can decide whether to emit telemetry. Emission is deferred until
547
+ // after phases 6 and 7 settle the final envelope ordering.
471
548
  let suppressed = false;
472
549
  for (let i = startIndex; i < envelopes.length; i++) {
473
550
  const env = envelopes[i]!;
@@ -480,9 +557,7 @@ function suppressCacheControlFrom(
480
557
  return rest as ProviderBlock;
481
558
  });
482
559
  }
483
- if (suppressed) {
484
- onEvent({ kind: 'cache_suppressed_for_synthetic', envelopeIndex: startIndex });
485
- }
560
+ return suppressed;
486
561
  }
487
562
 
488
563
  function validate(
@@ -111,7 +111,34 @@ export type NormalizeEvent =
111
111
  | { kind: 'synthetic_pending_result'; toolUseId: string; reason: 'trailing' | 'mid_stream' }
112
112
  | { kind: 'orphan_tool_result_textified'; toolUseId: string }
113
113
  | { kind: 'pending_in_flight'; toolUseId: string }
114
- | { kind: 'cache_suppressed_for_synthetic'; envelopeIndex: number };
114
+ | { kind: 'cache_suppressed_for_synthetic'; envelopeIndex: number }
115
+ | {
116
+ /**
117
+ * Fires when the first envelope after re-roling is assistant and a
118
+ * synthetic `[continuing]` user envelope had to be prepended to
119
+ * satisfy Anthropic's `messages[0].role === 'user'` requirement.
120
+ *
121
+ * `originalFirstRole` distinguishes the two causes a consumer might
122
+ * want to alert on separately:
123
+ * - `'user'` → re-roling artifact (a strict-role block lived
124
+ * under a user-role message and was moved to a
125
+ * new assistant envelope). Usually benign.
126
+ * - `'assistant'`→ producer shipped an assistant-first messages
127
+ * list. Real producer bug worth investigating.
128
+ *
129
+ * (Empty input never reaches this event: `rebuildEnvelopes([])`
130
+ * returns `[]`, and the phase-7 gate requires a non-empty envelope
131
+ * list. So `input[0]` is always defined when this fires.)
132
+ *
133
+ * `leadingBlockTypes` is the block-type list of the now-second
134
+ * envelope (i.e. what came right after the synthesized user turn),
135
+ * useful for classifying re-roling causes (e.g. `['thinking']`
136
+ * vs. `['text', 'tool_use']`).
137
+ */
138
+ kind: 'leading_user_synthesized';
139
+ originalFirstRole: 'user' | 'assistant';
140
+ leadingBlockTypes: string[];
141
+ };
115
142
 
116
143
  // ============================================================================
117
144
  // Build Result
package/src/membrane.ts CHANGED
@@ -292,6 +292,12 @@ export class Membrane {
292
292
  // These can't be handled by the text-based XML parser, so we capture and append them
293
293
  const extraContentBlocks: ContentBlock[] = [];
294
294
 
295
+ // Native thinking blocks from the provider (with signatures). The parser
296
+ // derives signature-less thinking blocks from <thinking> text (via
297
+ // wrapThinkingTags); signatures from these are merged into those after
298
+ // parsing, and signature-only blocks are prepended.
299
+ const providerThinkingBlocks: ContentBlock[] = [];
300
+
295
301
  // Transform initial request using the formatter
296
302
  let { providerRequest, prefillResult } = this.transformRequest(request, formatter);
297
303
 
@@ -385,6 +391,10 @@ export class Membrane {
385
391
  {
386
392
  signal,
387
393
  normalizedRequest: request,
394
+ // The tag-based parser tracks thinking via <thinking> tags — ask the
395
+ // provider to wrap native thinking deltas so they don't stream as
396
+ // visible text (see ProviderRequestOptions.wrapThinkingTags)
397
+ wrapThinkingTags: true,
388
398
  onRequest: (req) => {
389
399
  rawRequest = req;
390
400
  onRequest?.(req);
@@ -410,6 +420,18 @@ export class Membrane {
410
420
  data: (block as any).data,
411
421
  mimeType: (block as any).mimeType,
412
422
  } as ContentBlock);
423
+ } else if (block.type === 'thinking') {
424
+ // Native thinking block from the provider — carries the signature
425
+ // (encrypted full reasoning). Captured so consumers can persist and
426
+ // round-trip it for reasoning continuity. Includes signature-only
427
+ // blocks (display:'omitted' returns an empty thinking field).
428
+ providerThinkingBlocks.push({
429
+ type: 'thinking',
430
+ thinking: (block as any).thinking ?? '',
431
+ ...((block as any).signature ? { signature: (block as any).signature } : {}),
432
+ } as ContentBlock);
433
+ } else if (block.type === 'redacted_thinking') {
434
+ providerThinkingBlocks.push({ ...(block as any) } as ContentBlock);
413
435
  }
414
436
  }
415
437
  }
@@ -700,6 +722,33 @@ export class Membrane {
700
722
  response.content.push(...extraContentBlocks);
701
723
  }
702
724
 
725
+ // Merge provider thinking signatures into parser-derived thinking blocks
726
+ // (matched in stream order), and prepend any leftover provider blocks —
727
+ // signature-only thinking (display:'omitted') never appears in the text
728
+ // stream, so the parser produces no block for it. redacted_thinking
729
+ // blocks are always prepended verbatim.
730
+ if (providerThinkingBlocks.length > 0) {
731
+ const parsedThinking = response.content.filter(
732
+ (b) => b.type === 'thinking'
733
+ ) as Array<{ type: 'thinking'; thinking: string; signature?: string }>;
734
+
735
+ const providerThinking = providerThinkingBlocks.filter((b) => b.type === 'thinking');
736
+ const redacted = providerThinkingBlocks.filter((b) => b.type === 'redacted_thinking');
737
+
738
+ const matched = Math.min(providerThinking.length, parsedThinking.length);
739
+ for (let i = 0; i < matched; i++) {
740
+ const sig = (providerThinking[i] as { signature?: string }).signature;
741
+ if (sig) {
742
+ parsedThinking[i]!.signature = sig;
743
+ }
744
+ }
745
+
746
+ const leftover = providerThinking.slice(matched);
747
+ if (leftover.length > 0 || redacted.length > 0) {
748
+ response.content.unshift(...leftover, ...redacted);
749
+ }
750
+ }
751
+
703
752
  return response;
704
753
  } catch (error) {
705
754
  // Check if this is an abort error
@@ -1005,6 +1054,19 @@ export class Membrane {
1005
1054
  content: block.content,
1006
1055
  is_error: block.isError,
1007
1056
  });
1057
+ } else if (block.type === 'thinking') {
1058
+ // Round-trip thinking blocks verbatim including the signature — the
1059
+ // API validates it and (on display:'omitted' models) decrypts it to
1060
+ // reconstruct prior reasoning. Empty thinking + signature is valid.
1061
+ content.push({
1062
+ type: 'thinking',
1063
+ thinking: (block as { thinking?: string }).thinking ?? '',
1064
+ ...((block as { signature?: string }).signature
1065
+ ? { signature: (block as { signature?: string }).signature }
1066
+ : {}),
1067
+ });
1068
+ } else if (block.type === 'redacted_thinking') {
1069
+ content.push({ ...(block as unknown as Record<string, unknown>) });
1008
1070
  } else if (block.type === 'image') {
1009
1071
  if (block.source.type === 'base64') {
1010
1072
  const imageBlock: Record<string, unknown> = {
@@ -1081,13 +1143,8 @@ export class Membrane {
1081
1143
  );
1082
1144
  }
1083
1145
 
1084
- // Build thinking config for native extended thinking
1085
- const thinking = request.config.thinking?.enabled
1086
- ? {
1087
- type: 'enabled' as const,
1088
- budget_tokens: request.config.thinking.budgetTokens ?? 5000,
1089
- }
1090
- : undefined;
1146
+ // Build thinking config for native extended thinking (budget clamped to max_tokens)
1147
+ const thinking = this.buildThinkingParam(request.config);
1091
1148
 
1092
1149
  // Anthropic requires temperature=1 when extended thinking is enabled
1093
1150
  const temperature = thinking ? 1 : request.config.temperature;
@@ -1172,8 +1229,10 @@ export class Membrane {
1172
1229
  * Used by transformRequest, buildContinuationRequest, and buildContinuationRequestWithImages.
1173
1230
  */
1174
1231
  private getBaseProviderParams(config: NormalizedRequest['config']) {
1232
+ // Build thinking config for native extended thinking
1233
+ const thinking = this.buildThinkingParam(config);
1175
1234
  // Anthropic requires temperature=1 when extended thinking is enabled
1176
- const temperature = config.thinking?.enabled ? 1 : config.temperature;
1235
+ const temperature = thinking ? 1 : config.temperature;
1177
1236
  return {
1178
1237
  model: config.model,
1179
1238
  maxTokens: config.maxTokens,
@@ -1182,9 +1241,41 @@ export class Membrane {
1182
1241
  topK: config.topK,
1183
1242
  presencePenalty: config.presencePenalty,
1184
1243
  frequencyPenalty: config.frequencyPenalty,
1244
+ repetitionPenalty: config.repetitionPenalty,
1245
+ thinking,
1185
1246
  };
1186
1247
  }
1187
1248
 
1249
+ /**
1250
+ * Build the provider thinking parameter from config.
1251
+ *
1252
+ * For type 'enabled', the API requires max_tokens > budget_tokens and a
1253
+ * minimum budget of 1024 — a misconfigured budget (e.g., default 10000 with
1254
+ * max_tokens 4096) is clamped to fit. If no valid budget fits (max_tokens
1255
+ * too small), thinking is omitted entirely rather than sending a request
1256
+ * the API will reject.
1257
+ */
1258
+ private buildThinkingParam(config: NormalizedRequest['config']):
1259
+ | { type: 'adaptive'; display?: 'summarized' | 'omitted' }
1260
+ | { type: 'enabled'; budget_tokens: number; display?: 'summarized' | 'omitted' }
1261
+ | undefined {
1262
+ if (!config.thinking?.enabled) return undefined;
1263
+
1264
+ const display = config.thinking.display;
1265
+ if ((config.thinking.type ?? 'enabled') === 'adaptive') {
1266
+ return { type: 'adaptive', ...(display ? { display } : {}) };
1267
+ }
1268
+
1269
+ const requested = config.thinking.budgetTokens ?? 5000;
1270
+ const maxTokens = typeof config.maxTokens === 'number' ? config.maxTokens : undefined;
1271
+ const budget = maxTokens !== undefined ? Math.min(requested, maxTokens - 1024) : requested;
1272
+ if (budget < 1024) {
1273
+ // Can't fit a valid thinking budget under max_tokens — skip thinking
1274
+ return undefined;
1275
+ }
1276
+ return { type: 'enabled', budget_tokens: budget, ...(display ? { display } : {}) };
1277
+ }
1278
+
1188
1279
  /**
1189
1280
  * Transform a normalized request into provider format using the formatter
1190
1281
  */
@@ -1232,6 +1323,15 @@ export class Membrane {
1232
1323
  },
1233
1324
  };
1234
1325
 
1326
+ // The API rejects extended thinking combined with an assistant prefill.
1327
+ // Prefill-style builds (XML formatter) use the thinking config for the
1328
+ // literal `<thinking>` text prefix instead of the API feature — drop the
1329
+ // API param when the built request actually ends in an assistant prefill.
1330
+ // Chat-style builds (no prefill) keep it.
1331
+ if (buildResult.assistantPrefill && providerRequest.thinking) {
1332
+ delete providerRequest.thinking;
1333
+ }
1334
+
1235
1335
  return { providerRequest, prefillResult: buildResult };
1236
1336
  }
1237
1337
 
@@ -1243,6 +1343,8 @@ export class Membrane {
1243
1343
  timeoutMs?: number;
1244
1344
  idleTimeoutMs?: number;
1245
1345
  onRequest?: (rawRequest: unknown) => void;
1346
+ /** See ProviderRequestOptions.wrapThinkingTags */
1347
+ wrapThinkingTags?: boolean;
1246
1348
  /**
1247
1349
  * The original NormalizedRequest, threaded through so the
1248
1350
  * `beforeRequest` hook can see both shapes (normalized + provider).
@@ -1292,6 +1394,9 @@ export class Membrane {
1292
1394
 
1293
1395
  return {
1294
1396
  ...this.getBaseProviderParams(originalRequest.config),
1397
+ // Continuations always end in an assistant prefill — the API rejects
1398
+ // extended thinking combined with prefill, so never send the param here
1399
+ thinking: undefined,
1295
1400
  messages,
1296
1401
  system: prefillResult.systemContent
1297
1402
  ? (Array.isArray(prefillResult.systemContent) && prefillResult.systemContent.length > 0
@@ -1362,6 +1467,9 @@ export class Membrane {
1362
1467
 
1363
1468
  return {
1364
1469
  ...this.getBaseProviderParams(originalRequest.config),
1470
+ // Continuations always end in an assistant prefill — the API rejects
1471
+ // extended thinking combined with prefill, so never send the param here
1472
+ thinking: undefined,
1365
1473
  messages,
1366
1474
  system: prefillResult.systemContent
1367
1475
  ? (Array.isArray(prefillResult.systemContent) && prefillResult.systemContent.length > 0
@@ -1595,6 +1703,11 @@ export class Membrane {
1595
1703
  return 'stop_sequence';
1596
1704
  case 'tool_use':
1597
1705
  return 'tool_use';
1706
+ case 'refusal':
1707
+ // Safety refusal (e.g., Fable 5 reasoning_extraction). Must survive
1708
+ // mapping — downstream consumers react to refusals (chapterx adds a
1709
+ // Discord reaction). Defaulting this to end_turn silently hid them.
1710
+ return 'refusal';
1598
1711
  default:
1599
1712
  return 'end_turn';
1600
1713
  }
@@ -2483,13 +2596,16 @@ export class Membrane {
2483
2596
  }
2484
2597
 
2485
2598
  // Native tool names must match ^[a-zA-Z0-9_-]{1,128}$.
2486
- // The framework uses module:tool namespacing, so we round-trip colons
2487
- // through an escape encoding for the API wire format.
2488
- // Lossless: escape underscores first (_u), then encode colons (_c).
2599
+ // Tool names use `--` namespacing, which is already API-valid; the only
2600
+ // character that ever needs escaping is a literal colon, encoded losslessly as
2601
+ // `__` and back. We deliberately do NOT escape underscores they are valid,
2602
+ // and escaping them (the previous `_u`/`_c` scheme) garbled every
2603
+ // underscore-containing tool name in the request the model actually sees
2604
+ // (`send_message` → `send_umessage`), polluting its reasoning for no benefit.
2489
2605
  function sanitizeToolName(name: string): string {
2490
- return name.replace(/_/g, '_u').replace(/:/g, '_c');
2606
+ return name.replace(/:/g, '__');
2491
2607
  }
2492
2608
 
2493
2609
  function unsanitizeToolName(name: string): string {
2494
- return name.replace(/_c/g, ':').replace(/_u/g, '_');
2610
+ return name.replace(/__/g, ':');
2495
2611
  }