@spinabot/brigade 0.1.0

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 (39) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/SECURITY.md +208 -0
  5. package/assets/brigade-wordmark-on-black.png +0 -0
  6. package/assets/brigade-wordmark.png +0 -0
  7. package/brigade.mjs +96 -0
  8. package/dist/cli/chat-cmd.js +120 -0
  9. package/dist/cli/config-cmd.js +132 -0
  10. package/dist/cli/connect-cmd.js +447 -0
  11. package/dist/cli/doctor-cmd.js +317 -0
  12. package/dist/cli/gateway-cmd.js +92 -0
  13. package/dist/cli.js +287 -0
  14. package/dist/core/agent.js +1123 -0
  15. package/dist/core/config.js +80 -0
  16. package/dist/core/console-stream.js +188 -0
  17. package/dist/core/error-classifier.js +354 -0
  18. package/dist/core/event-logger.js +122 -0
  19. package/dist/core/model-caps.js +185 -0
  20. package/dist/core/provider-payload-mutators.js +517 -0
  21. package/dist/core/provider-quirks.js +285 -0
  22. package/dist/core/server.js +459 -0
  23. package/dist/core/smart-compaction.js +209 -0
  24. package/dist/core/system-prompt-defaults.js +88 -0
  25. package/dist/core/system-prompt-guidance.js +269 -0
  26. package/dist/core/system-prompt.js +884 -0
  27. package/dist/index.js +30 -0
  28. package/dist/integrations/ollama.js +140 -0
  29. package/dist/protocol.js +49 -0
  30. package/dist/providers/catalog.js +100 -0
  31. package/dist/providers/validate-key.js +197 -0
  32. package/dist/tui/client.js +263 -0
  33. package/dist/ui/brand-frames-cli.js +20 -0
  34. package/dist/ui/brand-frames.js +36 -0
  35. package/dist/ui/brand.js +402 -0
  36. package/dist/ui/chat.js +929 -0
  37. package/dist/ui/onboarding.js +400 -0
  38. package/dist/ui/theme.js +51 -0
  39. package/package.json +92 -0
@@ -0,0 +1,517 @@
1
+ /**
2
+ * Provider-payload mutators — fire INSIDE Pi's `onPayload` hook to mutate
3
+ * the assembled provider-specific JSON right before it ships over HTTP.
4
+ *
5
+ * Pi's wrapping order is:
6
+ * session.prompt → Pi's auth wrapper → provider adapter → buildPayload
7
+ * → options.onPayload?.(payload, model) ← we hook here
8
+ * → HTTP request
9
+ *
10
+ * We wrap `session.agent.streamFn` (preserving Pi's existing auth wrapper)
11
+ * to inject our own `onPayload` callback. The callback delegates to any
12
+ * caller-supplied `onPayload` first, then walks the provider mutators in
13
+ * order. Each mutator is a pure-ish function gated on the active model;
14
+ * no-ops when the provider doesn't match.
15
+ *
16
+ * Three provider quirks are handled here:
17
+ * - OpenRouter Anthropic prompt-cache hints (cost win on long sessions)
18
+ * - Google Gemini thinking-config payload reformat (Pi's level → Google's enum)
19
+ * - SiliconFlow / Minimax thinking-mode normalization
20
+ *
21
+ * NOT a replacement for Pi's auth wrapper — strictly additive. The wrapped
22
+ * streamFn calls `originalStreamFn(model, context, options)` underneath.
23
+ */
24
+ import { BRIGADE_CACHE_BOUNDARY } from "./system-prompt.js";
25
+ /* ─────────────────────────── 1. Anthropic system-prompt cache hints ─────────────────────────── */
26
+ /**
27
+ * Apply Anthropic prompt-cache markers to the system field. Three cases:
28
+ *
29
+ * A. SYSTEM PROMPT CONTAINS BRIGADE_CACHE_BOUNDARY MARKER
30
+ * Split the system text at the marker into two `{ type: "text" }`
31
+ * blocks. Apply `cache_control: { type: "ephemeral" }` to the FIRST
32
+ * block only (the static prefix). The SECOND block (dynamic suffix —
33
+ * runtime info, model id, etc.) gets no marker, so it doesn't break
34
+ * the cache when it varies per turn.
35
+ *
36
+ * B. NO MARKER, SYSTEM IS A STRING
37
+ * Convert to a single text block WITH cache_control. Whole prompt is
38
+ * cached. (This is the legacy behavior of `applyOpenRouterAnthropicCacheHints`.)
39
+ *
40
+ * C. NO MARKER, SYSTEM IS ALREADY AN ARRAY
41
+ * Mark the last text block with cache_control. (Same legacy behavior.)
42
+ *
43
+ * Then mark the LAST USER MESSAGE'S last content block with cache_control
44
+ * — this is the second of Anthropic's up-to-4 breakpoints (we use 2: system
45
+ * prefix + last user message).
46
+ *
47
+ * Only fires when `isAnthropicFlavored(model)` — direct Anthropic, OpenRouter
48
+ * routing to Claude, Anthropic-compat APIs (Bedrock, Vertex, Minimax). For
49
+ * non-Anthropic providers, see `stripCacheBoundaryFromPayload` below — the
50
+ * marker is stripped so the model never sees it.
51
+ *
52
+ * Replaces the older `applyOpenRouterAnthropicCacheHints`. The new version
53
+ * handles the boundary marker (which is what enables the static-vs-dynamic
54
+ * split that gives us the 5-10× cost win on multi-turn).
55
+ */
56
+ export function applyAnthropicSystemCacheHints(payload, model) {
57
+ if (!isAnthropicFlavored(model))
58
+ return;
59
+ if (!payload || typeof payload !== "object")
60
+ return;
61
+ const p = payload;
62
+ const cacheMarker = { type: "ephemeral" };
63
+ // Mark the system field — string → array, array → split or mark last.
64
+ applyCacheToSystem(p, cacheMarker);
65
+ // Mark the last user message — second cache breakpoint.
66
+ applyCacheToLastUserMessage(p, cacheMarker);
67
+ }
68
+ function applyCacheToSystem(payload, cacheMarker) {
69
+ // Pi may pass system as either a top-level `system` string/array OR as
70
+ // a `messages` entry with role: "system". We handle both shapes.
71
+ if (typeof payload.system === "string") {
72
+ payload.system = splitOrWrap(payload.system, cacheMarker);
73
+ return;
74
+ }
75
+ if (Array.isArray(payload.system)) {
76
+ payload.system = applyCacheToBlocks(payload.system, cacheMarker);
77
+ return;
78
+ }
79
+ // Fallback: mutate the messages array's system role entry.
80
+ const messages = payload.messages;
81
+ if (!Array.isArray(messages))
82
+ return;
83
+ for (const msg of messages) {
84
+ if (msg?.role !== "system" && msg?.role !== "developer")
85
+ continue;
86
+ if (typeof msg.content === "string") {
87
+ msg.content = splitOrWrap(msg.content, cacheMarker);
88
+ }
89
+ else if (Array.isArray(msg.content)) {
90
+ msg.content = applyCacheToBlocks(msg.content, cacheMarker);
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * Convert a system-prompt STRING into one or two `{type:"text"}` blocks.
96
+ * If the string contains the BRIGADE_CACHE_BOUNDARY marker, split into:
97
+ * - block 1: stable prefix WITH cache_control
98
+ * - block 2: dynamic suffix WITHOUT cache_control (changes per turn)
99
+ * Otherwise wrap the whole string in a single block with cache_control.
100
+ */
101
+ function splitOrWrap(text, cacheMarker) {
102
+ const idx = text.indexOf(BRIGADE_CACHE_BOUNDARY);
103
+ if (idx === -1) {
104
+ return [{ type: "text", text, cache_control: cacheMarker }];
105
+ }
106
+ const stablePrefix = text.slice(0, idx).replace(/\s+$/, "");
107
+ const dynamicSuffix = text.slice(idx + BRIGADE_CACHE_BOUNDARY.length).replace(/^\s+/, "");
108
+ const blocks = [];
109
+ if (stablePrefix.length > 0) {
110
+ blocks.push({ type: "text", text: stablePrefix, cache_control: cacheMarker });
111
+ }
112
+ if (dynamicSuffix.length > 0) {
113
+ blocks.push({ type: "text", text: dynamicSuffix });
114
+ }
115
+ // If both halves trim to empty (degenerate input — caller passed a system
116
+ // prompt that's nothing but whitespace + a marker), return a single empty
117
+ // block WITHOUT cache_control. Anthropic rejects empty cache blocks
118
+ // (`{type:"text", text:"", cache_control:...}`) but accepts an empty
119
+ // non-cached block. Conservative fallback so the request still flies.
120
+ return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
121
+ }
122
+ /**
123
+ * Walk an existing array of system blocks looking for the boundary marker.
124
+ * If a text block contains it, split that block in place. If no block
125
+ * contains it, mark the LAST non-thinking block with cache_control (legacy
126
+ * behavior — caches the whole prefix up to that point).
127
+ */
128
+ function applyCacheToBlocks(blocks, cacheMarker) {
129
+ let foundBoundary = false;
130
+ const result = [];
131
+ for (const block of blocks) {
132
+ if (!foundBoundary &&
133
+ block &&
134
+ typeof block === "object" &&
135
+ block.type === "text" &&
136
+ typeof block.text === "string" &&
137
+ block.text.includes(BRIGADE_CACHE_BOUNDARY)) {
138
+ // Split this block at the boundary marker. Spread the source block's
139
+ // other fields (e.g. citations, custom keys) FIRST, then the new
140
+ // block's fields, then explicitly drop any inherited `cache_control`
141
+ // so the dynamic-suffix block (which `splitOrWrap` deliberately
142
+ // emits without cache_control) doesn't accidentally re-inherit one
143
+ // from the source. If the source block had `cache_control` already,
144
+ // silently letting it through would mark the suffix as cached even
145
+ // though it changes per turn — guaranteed cache miss every turn.
146
+ const split = splitOrWrap(block.text, cacheMarker);
147
+ for (const newBlock of split) {
148
+ const merged = { ...block, ...newBlock };
149
+ if (newBlock.cache_control === undefined) {
150
+ delete merged.cache_control;
151
+ }
152
+ result.push(merged);
153
+ }
154
+ foundBoundary = true;
155
+ }
156
+ else {
157
+ result.push(block);
158
+ }
159
+ }
160
+ // No boundary marker anywhere — mark the LAST non-thinking block.
161
+ if (!foundBoundary) {
162
+ for (let i = result.length - 1; i >= 0; i--) {
163
+ const block = result[i];
164
+ if (block && typeof block === "object" && block.type !== "thinking") {
165
+ if (block.cache_control === undefined) {
166
+ block.cache_control = cacheMarker;
167
+ }
168
+ break;
169
+ }
170
+ }
171
+ }
172
+ return result;
173
+ }
174
+ /**
175
+ * Apply cache_control to the LAST content block of the LAST user message.
176
+ * This is the second of Anthropic's 2 cache breakpoints (system + last
177
+ * user).
178
+ *
179
+ * IMPORTANT — Anthropic caps the request at 4 cache breakpoints total. Pi
180
+ * persists user messages across turns, so any cache_control we attached on
181
+ * a PREVIOUS turn is still on the older user/tool_result blocks. Without
182
+ * the sweep below, breakpoints accumulate turn-over-turn (turn N adds one
183
+ * to a new user msg; turn N+1 adds another while the old one is still set;
184
+ * etc.) and quickly exceed the cap → Anthropic 400. Best practice is "only
185
+ * the last user message holds the live breakpoint" — so we clear every
186
+ * older user-side breakpoint before applying the new one.
187
+ *
188
+ * Only applies if the message is a user message (skip if last message
189
+ * happens to be assistant — shouldn't happen in a healthy conversation
190
+ * but guard anyway).
191
+ */
192
+ function applyCacheToLastUserMessage(payload, cacheMarker) {
193
+ const messages = payload.messages;
194
+ if (!Array.isArray(messages) || messages.length === 0)
195
+ return;
196
+ // Sweep: clear cache_control from EVERY user/tool_result block in EVERY
197
+ // message except the last one. We restrict the sweep to user-side roles
198
+ // because Pi may set cache_control on assistant-emitted blocks for its
199
+ // own reasons (rare, but not ours to clobber).
200
+ for (let i = 0; i < messages.length - 1; i++) {
201
+ const msg = messages[i];
202
+ if (!msg || typeof msg !== "object")
203
+ continue;
204
+ if (msg.role !== "user")
205
+ continue;
206
+ const content = msg.content;
207
+ if (!Array.isArray(content))
208
+ continue;
209
+ for (const block of content) {
210
+ if (block &&
211
+ typeof block === "object" &&
212
+ (block.type === "text" ||
213
+ block.type === "image" ||
214
+ block.type === "tool_result") &&
215
+ block.cache_control !== undefined) {
216
+ delete block.cache_control;
217
+ }
218
+ }
219
+ }
220
+ const last = messages[messages.length - 1];
221
+ if (!last || typeof last !== "object" || last.role !== "user")
222
+ return;
223
+ if (Array.isArray(last.content) && last.content.length > 0) {
224
+ const lastBlock = last.content[last.content.length - 1];
225
+ if (lastBlock &&
226
+ typeof lastBlock === "object" &&
227
+ (lastBlock.type === "text" ||
228
+ lastBlock.type === "image" ||
229
+ lastBlock.type === "tool_result")) {
230
+ if (lastBlock.cache_control === undefined) {
231
+ lastBlock.cache_control = cacheMarker;
232
+ }
233
+ }
234
+ }
235
+ else if (typeof last.content === "string") {
236
+ // Convert string to array form so we can attach cache_control.
237
+ last.content = [
238
+ { type: "text", text: last.content, cache_control: cacheMarker },
239
+ ];
240
+ }
241
+ }
242
+ /**
243
+ * Whether the model is Anthropic-flavored — direct Anthropic, OpenRouter→
244
+ * Claude, or any provider whose API field is `anthropic-messages`. Used to
245
+ * gate `applyAnthropicSystemCacheHints`.
246
+ */
247
+ export function isAnthropicFlavored(model) {
248
+ if (!model)
249
+ return false;
250
+ const api = (model.api ?? "").toLowerCase();
251
+ if (api === "anthropic" || api === "anthropic-messages")
252
+ return true;
253
+ const provider = (model.provider ?? "").toLowerCase();
254
+ if (provider === "anthropic")
255
+ return true;
256
+ if (provider === "minimax" && api === "anthropic-messages")
257
+ return true;
258
+ if (provider === "openrouter" && /(?:^|\/)(?:anthropic|claude)/i.test(model.id ?? "")) {
259
+ return true;
260
+ }
261
+ if (provider === "bedrock" || provider === "vertex") {
262
+ return /claude|anthropic/i.test(model.id ?? "");
263
+ }
264
+ return false;
265
+ }
266
+ /**
267
+ * Backwards-compat aliases. The old names (`isOpenRouterAnthropic`,
268
+ * `applyOpenRouterAnthropicCacheHints`) are too narrow now that the cache
269
+ * mutator handles direct Anthropic + Bedrock + Vertex + Minimax too. Kept
270
+ * exported under the old names so test files and any external callers
271
+ * continue to compile during the transition.
272
+ *
273
+ * @deprecated use `isAnthropicFlavored` / `applyAnthropicSystemCacheHints`.
274
+ */
275
+ export const isOpenRouterAnthropic = isAnthropicFlavored;
276
+ export const applyOpenRouterAnthropicCacheHints = applyAnthropicSystemCacheHints;
277
+ /**
278
+ * Strip the BRIGADE_CACHE_BOUNDARY marker from any system field.
279
+ * Universal pre-pass — runs for ALL providers (Anthropic too) so the
280
+ * marker never escapes Brigade. For Anthropic, the marker has already
281
+ * been used by `applyAnthropicSystemCacheHints` to split the prompt;
282
+ * stripping leftovers is defensive in case the splitter missed one
283
+ * (multiple markers, marker in array form Pi assembled differently, etc).
284
+ *
285
+ * For non-Anthropic providers, this is the ONLY thing that touches the
286
+ * system field — strip the marker, leave the prompt as a single string
287
+ * the model can read.
288
+ */
289
+ export function stripCacheBoundaryFromPayload(payload) {
290
+ if (!payload || typeof payload !== "object")
291
+ return;
292
+ const p = payload;
293
+ if (typeof p.system === "string") {
294
+ p.system = p.system.split(BRIGADE_CACHE_BOUNDARY).join("\n").trim();
295
+ }
296
+ else if (Array.isArray(p.system)) {
297
+ for (const block of p.system) {
298
+ if (block && typeof block === "object" && typeof block.text === "string") {
299
+ block.text = block.text.split(BRIGADE_CACHE_BOUNDARY).join("\n").trim();
300
+ }
301
+ }
302
+ }
303
+ const messages = p.messages;
304
+ if (Array.isArray(messages)) {
305
+ for (const msg of messages) {
306
+ if (msg?.role !== "system" && msg?.role !== "developer")
307
+ continue;
308
+ if (typeof msg.content === "string") {
309
+ msg.content = msg.content.split(BRIGADE_CACHE_BOUNDARY).join("\n").trim();
310
+ }
311
+ else if (Array.isArray(msg.content)) {
312
+ for (const block of msg.content) {
313
+ if (block && typeof block === "object" && typeof block.text === "string") {
314
+ block.text = block.text.split(BRIGADE_CACHE_BOUNDARY).join("\n").trim();
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ /* ─────────────────────────── 2. Google Gemini thinking payload ─────────────────────────── */
322
+ /**
323
+ * Gemini's GenerateContent API expects thinking config under
324
+ * `config.thinkingConfig.thinkingLevel` with values from a fixed enum:
325
+ * "MINIMAL" | "LOW" | "MEDIUM" | "HIGH". Pi's ThinkingLevel uses lowercase
326
+ * "minimal" | "low" | "medium" | "high" | "xhigh" (xhigh is Pi-only).
327
+ *
328
+ * The fix: write the uppercased value AND drop `xhigh` (which Gemini
329
+ * rejects as unknown).
330
+ *
331
+ * This is purely a payload-shape concern — Pi's adapter already builds the
332
+ * `config.thinkingConfig` object; we just normalize the level string.
333
+ */
334
+ export function sanitizeGeminiThinkingPayload(payload, model) {
335
+ if (!isGoogleGeminiModel(model))
336
+ return;
337
+ if (!payload || typeof payload !== "object")
338
+ return;
339
+ const cfg = payload.config;
340
+ if (!cfg || typeof cfg !== "object")
341
+ return;
342
+ const tc = cfg.thinkingConfig;
343
+ if (!tc || typeof tc !== "object")
344
+ return;
345
+ const raw = tc.thinkingLevel ?? tc.thinking_level;
346
+ if (typeof raw !== "string")
347
+ return;
348
+ const mapped = mapPiThinkingToGemini(raw);
349
+ if (mapped === undefined) {
350
+ // Unknown level → drop the config entirely so Gemini uses its default
351
+ // instead of rejecting an invalid enum.
352
+ delete cfg.thinkingConfig;
353
+ return;
354
+ }
355
+ tc.thinkingLevel = mapped;
356
+ // Some Gemini variants want it under thinking_level (snake_case). Set both
357
+ // when one was supplied, so we don't silently lose the value if Pi later
358
+ // switches naming.
359
+ if ("thinking_level" in tc)
360
+ tc.thinking_level = mapped;
361
+ }
362
+ function mapPiThinkingToGemini(level) {
363
+ switch (level.toLowerCase()) {
364
+ case "off":
365
+ return undefined; // Gemini has no "off" — drop the config
366
+ case "minimal":
367
+ return "MINIMAL";
368
+ case "low":
369
+ return "LOW";
370
+ case "medium":
371
+ return "MEDIUM";
372
+ case "high":
373
+ case "xhigh": // xhigh isn't supported; bucket it as HIGH
374
+ return "HIGH";
375
+ default:
376
+ return undefined;
377
+ }
378
+ }
379
+ export function isGoogleGeminiModel(model) {
380
+ if (!model)
381
+ return false;
382
+ const api = (model.api ?? "").toLowerCase();
383
+ if (api === "google-generative-ai" || api === "google-gemini" || api === "google")
384
+ return true;
385
+ const provider = (model.provider ?? "").toLowerCase();
386
+ if (provider === "google" || provider === "gemini" || provider === "google-vertex" || provider === "vertex") {
387
+ return true;
388
+ }
389
+ if (provider === "openrouter" && /gemini|google/i.test(model.id ?? ""))
390
+ return true;
391
+ return false;
392
+ }
393
+ /* ─────────────────────────── 3. SiliconFlow thinking-off normalization ─────────────────────────── */
394
+ /**
395
+ * SiliconFlow rejects `thinking: "off"` (Pi's value for "no reasoning") and
396
+ * wants `thinking: null` instead — this mutator does the swap.
397
+ *
398
+ * Pi sets `thinking: "off"` automatically when `session.thinkingLevel === "off"`
399
+ * for OpenAI-compatible providers; SiliconFlow shares the OpenAI shape
400
+ * but is stricter about this one field.
401
+ */
402
+ export function normalizeSiliconFlowThinking(payload, model) {
403
+ if (!isSiliconFlowModel(model))
404
+ return;
405
+ if (!payload || typeof payload !== "object")
406
+ return;
407
+ const p = payload;
408
+ if (p.thinking === "off") {
409
+ p.thinking = null;
410
+ }
411
+ }
412
+ export function isSiliconFlowModel(model) {
413
+ if (!model)
414
+ return false;
415
+ const provider = (model.provider ?? "").toLowerCase();
416
+ if (provider === "siliconflow" || provider === "silicon-flow")
417
+ return true;
418
+ const baseUrl = model.baseUrl ?? "";
419
+ if (typeof baseUrl === "string" && /siliconflow/i.test(baseUrl))
420
+ return true;
421
+ return false;
422
+ }
423
+ /* ─────────────────────────── 4. Minimax force-disable thinking ─────────────────────────── */
424
+ /**
425
+ * Minimax exposes an Anthropic-shaped Messages endpoint but ALWAYS streams
426
+ * reasoning back as visible deltas — even when the user wants thinking
427
+ * suppressed. The fix is to inject `thinking: { type: "disabled" }` into
428
+ * the payload when it's missing, so Minimax stops emitting reasoning deltas.
429
+ *
430
+ * Only applies when the underlying API is Anthropic Messages AND the
431
+ * provider is Minimax. Other Anthropic-compatible endpoints don't need this.
432
+ */
433
+ export function disableMinimaxThinking(payload, model) {
434
+ if (!isMinimaxAnthropicModel(model))
435
+ return;
436
+ if (!payload || typeof payload !== "object")
437
+ return;
438
+ const p = payload;
439
+ // Don't overwrite an explicit thinking config the user set elsewhere.
440
+ if (p.thinking === undefined) {
441
+ p.thinking = { type: "disabled" };
442
+ }
443
+ }
444
+ export function isMinimaxAnthropicModel(model) {
445
+ if (!model)
446
+ return false;
447
+ const api = (model.api ?? "").toLowerCase();
448
+ if (api !== "anthropic-messages" && api !== "anthropic")
449
+ return false;
450
+ const provider = (model.provider ?? "").toLowerCase();
451
+ return provider === "minimax" || provider === "minimax-portal";
452
+ }
453
+ /* ─────────────────────────── composition ─────────────────────────── */
454
+ /**
455
+ * Run every applicable mutator against the payload. Each mutator self-gates
456
+ * on the active model — no-ops when the provider doesn't match — so calling
457
+ * `applyAll` for every request is cheap.
458
+ *
459
+ * Order matters:
460
+ * 1. Anthropic system-prompt cache hints — splits at the boundary marker
461
+ * and applies cache_control. Must run BEFORE the universal strip
462
+ * (otherwise the boundary is gone before we can split on it).
463
+ * 2. Universal boundary strip — removes any leftover marker so it never
464
+ * reaches the model regardless of provider. Idempotent for Anthropic
465
+ * (the splitter already removed it for matched cases); load-bearing
466
+ * for non-Anthropic.
467
+ * 3. Provider-specific mutators (Gemini thinking, SiliconFlow, Minimax) —
468
+ * operate on fields other than `system`, no ordering interaction with
469
+ * the cache work above.
470
+ *
471
+ * Mutates `payload` in place. Returns nothing (caller hands `payload` back
472
+ * to Pi unchanged-by-reference; the in-place mutation is what Pi sends).
473
+ */
474
+ export function applyAllPayloadMutations(payload, model) {
475
+ if (!model || !payload)
476
+ return;
477
+ applyAnthropicSystemCacheHints(payload, model);
478
+ stripCacheBoundaryFromPayload(payload);
479
+ sanitizeGeminiThinkingPayload(payload, model);
480
+ normalizeSiliconFlowThinking(payload, model);
481
+ disableMinimaxThinking(payload, model);
482
+ }
483
+ /**
484
+ * Wrap `session.agent.streamFn` so every request gets `applyAllPayloadMutations`
485
+ * fired against its assembled payload.
486
+ *
487
+ * SAFE TO CALL ONCE per session — preserves Pi's existing auth-aware streamFn
488
+ * by composing on top of it. Calling more than once would stack wrappers
489
+ * and fire mutations multiple times (idempotent for our current mutators
490
+ * but wasteful); buildAgent calls it exactly once after createAgentSession.
491
+ *
492
+ * If a caller (test, future feature) supplies its OWN `onPayload` in
493
+ * StreamOptions, we run THEIRS first and then our mutators on whatever
494
+ * they returned (or the original if they returned undefined). This keeps
495
+ * caller hooks composable.
496
+ */
497
+ export function wrapStreamFnWithPayloadMutations(session) {
498
+ const original = session.agent.streamFn;
499
+ if (!original)
500
+ return;
501
+ const wrapped = (model, context, options) => {
502
+ const userOnPayload = options?.onPayload;
503
+ const augmented = {
504
+ ...(options ?? {}),
505
+ onPayload: async (payload, m) => {
506
+ // User's hook gets first crack at mutating / replacing.
507
+ const userResult = userOnPayload ? await userOnPayload(payload, m) : undefined;
508
+ const next = userResult !== undefined ? userResult : payload;
509
+ applyAllPayloadMutations(next, m);
510
+ return next;
511
+ },
512
+ };
513
+ return original(model, context, augmented);
514
+ };
515
+ session.agent.streamFn = wrapped;
516
+ }
517
+ //# sourceMappingURL=provider-payload-mutators.js.map