@oh-my-pi/pi-ai 16.2.13 → 16.3.2

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.
@@ -4,7 +4,7 @@ import { scheduler } from "node:timers/promises";
4
4
  import * as tls from "node:tls";
5
5
  import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
6
6
  import { mapEffortToAnthropicAdaptiveEffort } from "@oh-my-pi/pi-catalog/model-thinking";
7
- import { calculateCost } from "@oh-my-pi/pi-catalog/models";
7
+ import { calculateCost, getBundledModel } from "@oh-my-pi/pi-catalog/models";
8
8
  import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils";
9
9
  import { parseGitHubCopilotApiKey } from "@oh-my-pi/pi-catalog/wire/github-copilot";
10
10
  import {
@@ -20,6 +20,7 @@ import { renderDemotedThinking } from "../dialect/demotion";
20
20
  import * as AIError from "../error";
21
21
  import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
22
22
  import type {
23
+ AnthropicFallbackContent,
23
24
  Api,
24
25
  AssistantMessage,
25
26
  CacheRetention,
@@ -73,7 +74,9 @@ import {
73
74
  import type {
74
75
  ToolInputSchema as AnthropicToolInputSchema,
75
76
  Tool as AnthropicWireTool,
77
+ Usage as AnthropicWireUsage,
76
78
  ContentBlockParam,
79
+ FallbackParam,
77
80
  MessageCreateParamsStreaming,
78
81
  MessageParam,
79
82
  RawMessageStreamEvent,
@@ -150,6 +153,7 @@ const redactThinkingBeta = "redact-thinking-2026-02-12";
150
153
  const fastModeBeta = "fast-mode-2026-02-01";
151
154
  const taskBudgetBeta = "task-budgets-2026-03-13";
152
155
  const effortBeta = "effort-2025-11-24";
156
+ const serverSideFallbackBeta = "server-side-fallback-2026-06-01";
153
157
 
154
158
  function buildClaudeCodeBetas(
155
159
  agentRequest: boolean,
@@ -1060,6 +1064,15 @@ export interface AnthropicOptions extends StreamOptions {
1060
1064
  * including SDK clients such as `AnthropicVertex`.
1061
1065
  */
1062
1066
  client?: AnthropicMessagesClientLike;
1067
+ /**
1068
+ * Server-side fallback beta chain (`server-side-fallback-2026-06-01`).
1069
+ * When set, `fallbacks` is forwarded on the request body and the beta
1070
+ * header is auto-attached; the response parser then honors mid-stream
1071
+ * `fallback` content blocks and `usage.iterations` for served-model
1072
+ * promotion and per-attempt pricing. Opt-in ONLY — leaving this
1073
+ * undefined preserves the pre-fallback behavior on every code path.
1074
+ */
1075
+ fallbacks?: FallbackParam[];
1063
1076
  }
1064
1077
 
1065
1078
  export type AnthropicClientOptionsArgs = {
@@ -1529,6 +1542,108 @@ export function applyAnthropicUsageExtras(usage: Usage, source: AnthropicUsageLi
1529
1542
  }
1530
1543
  }
1531
1544
 
1545
+ function parseAnthropicFallbackWireBlock(value: unknown): AnthropicFallbackContent | undefined {
1546
+ if (!isRecord(value) || value.type !== "fallback") return undefined;
1547
+ const from = isRecord(value.from) && typeof value.from.model === "string" ? value.from.model : undefined;
1548
+ const to = isRecord(value.to) && typeof value.to.model === "string" ? value.to.model : undefined;
1549
+ if (!from?.trim() || !to?.trim()) return undefined;
1550
+ return { type: "fallback", from: { model: from }, to: { model: to } };
1551
+ }
1552
+
1553
+ /**
1554
+ * The definitive "served by fallback" signal per Anthropic's fallback
1555
+ * billing cookbook (§4): a `fallback_message` iteration in `usage.iterations`.
1556
+ * Any other iteration type is per-attempt bookkeeping for the requested model
1557
+ * (including its dated snapshot alias) and MUST NOT retag the assistant turn.
1558
+ */
1559
+ function fallbackServedModelFromUsage(source: AnthropicWireUsage): string | undefined {
1560
+ const iterations = source.iterations ?? [];
1561
+ for (let index = iterations.length - 1; index >= 0; index -= 1) {
1562
+ const iteration = iterations[index];
1563
+ if (iteration?.type === "fallback_message" && iteration.model?.trim()) return iteration.model;
1564
+ }
1565
+ return undefined;
1566
+ }
1567
+
1568
+ /**
1569
+ * Price a fallback turn per the fallback billing cookbook §4:
1570
+ * • A pre-served attempt with zero output/cache-creation is not billed
1571
+ * (waived classifier block); its iteration is skipped.
1572
+ * • Mid-stream refusals bill their attempting model's input+output at
1573
+ * that model's normal rates.
1574
+ * • The `fallback_message` attempt's input tokens are rebilled at the
1575
+ * served model's cache-read rate (fallback credit — 10% of base input).
1576
+ *
1577
+ * Top-level `usage.input/output/cacheRead/cacheWrite` stay Anthropic's raw
1578
+ * served-attempt counts; `usage.cost` reflects the per-iteration attributed
1579
+ * total. Non-fallback turns skip this path entirely and use the requested
1580
+ * model at the normal `calculateCost` call.
1581
+ */
1582
+ /**
1583
+ * Resolve a served/iteration model id to its bundled catalog entry when
1584
+ * possible so the per-iteration cost uses the served model's pricing
1585
+ * (e.g. Opus 4.8 rates for a Fable→Opus fallback). Falls back to
1586
+ * `requestModel` when the id is empty, matches the request, or the
1587
+ * catalog has no entry under it — the caller keeps the requested-model
1588
+ * pricing as the safe default and logs at the source.
1589
+ */
1590
+ function resolveIterationModel(
1591
+ requestModel: Model<"anthropic-messages">,
1592
+ iterationModelId: string | null | undefined,
1593
+ ): Model<Api> {
1594
+ const id = iterationModelId?.trim();
1595
+ if (!id || id === requestModel.id) return requestModel;
1596
+ // Bundled catalog lookup: only Anthropic provider entries are safe to
1597
+ // reference (dated snapshots resolve to their alias entry when present).
1598
+ if (requestModel.provider === "anthropic") {
1599
+ const bundled = getBundledModel("anthropic", id);
1600
+ if (bundled?.api === "anthropic-messages") return bundled;
1601
+ }
1602
+ return requestModel;
1603
+ }
1604
+
1605
+ function calculateFallbackTurnCost(
1606
+ requestModel: Model<"anthropic-messages">,
1607
+ usage: Usage,
1608
+ source: AnthropicWireUsage,
1609
+ ): boolean {
1610
+ const iterations = source.iterations ?? [];
1611
+ if (iterations.length === 0) return false;
1612
+ const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
1613
+ const hasFallbackMessage = iterations.some(iter => iter.type === "fallback_message");
1614
+ let applied = false;
1615
+ for (const iteration of iterations) {
1616
+ const inputTokens = iteration.input_tokens ?? 0;
1617
+ const outputTokens = iteration.output_tokens ?? 0;
1618
+ const cacheReadTokens = iteration.cache_read_input_tokens ?? 0;
1619
+ const cacheWriteTokens = iteration.cache_creation_input_tokens ?? 0;
1620
+ const isFallback = iteration.type === "fallback_message";
1621
+ if (hasFallbackMessage && !isFallback && outputTokens === 0 && cacheWriteTokens === 0) continue;
1622
+ const iterationUsage = createEmptyUsage();
1623
+ if (isFallback) {
1624
+ iterationUsage.input = 0;
1625
+ iterationUsage.cacheRead = cacheReadTokens + inputTokens;
1626
+ } else {
1627
+ iterationUsage.input = inputTokens;
1628
+ iterationUsage.cacheRead = cacheReadTokens;
1629
+ }
1630
+ iterationUsage.output = outputTokens;
1631
+ iterationUsage.cacheWrite = cacheWriteTokens;
1632
+ iterationUsage.totalTokens =
1633
+ iterationUsage.input + iterationUsage.output + iterationUsage.cacheRead + iterationUsage.cacheWrite;
1634
+ calculateCost(resolveIterationModel(requestModel, iteration.model), iterationUsage);
1635
+ cost.input += iterationUsage.cost.input;
1636
+ cost.output += iterationUsage.cost.output;
1637
+ cost.cacheRead += iterationUsage.cost.cacheRead;
1638
+ cost.cacheWrite += iterationUsage.cost.cacheWrite;
1639
+ cost.total += iterationUsage.cost.total;
1640
+ applied = true;
1641
+ }
1642
+ if (!applied) return false;
1643
+ usage.cost = cost;
1644
+ return true;
1645
+ }
1646
+
1532
1647
  const streamAnthropicOnce = (
1533
1648
  model: Model<"anthropic-messages">,
1534
1649
  context: Context,
@@ -1644,6 +1759,27 @@ const streamAnthropicOnce = (
1644
1759
  ) {
1645
1760
  extraBetas.push(contextManagementBeta);
1646
1761
  }
1762
+ // Server-side fallback beta chain: opt-in via `options.fallbacks`.
1763
+ // Nested overrides (`speed`, `output_config.effort`,
1764
+ // `output_config.task_budget`) reuse the same top-level betas
1765
+ // Anthropic requires for the primary request, so scan the chain
1766
+ // and add every companion beta the fallback entries touch.
1767
+ if (options?.fallbacks?.length) {
1768
+ if (!extraBetas.includes(serverSideFallbackBeta)) {
1769
+ extraBetas.push(serverSideFallbackBeta);
1770
+ }
1771
+ for (const entry of options.fallbacks) {
1772
+ if (entry.speed === "fast" && !extraBetas.includes(fastModeBeta)) {
1773
+ extraBetas.push(fastModeBeta);
1774
+ }
1775
+ if (entry.output_config?.effort && !extraBetas.includes(effortBeta)) {
1776
+ extraBetas.push(effortBeta);
1777
+ }
1778
+ if (entry.output_config?.task_budget && !extraBetas.includes(taskBudgetBeta)) {
1779
+ extraBetas.push(taskBudgetBeta);
1780
+ }
1781
+ }
1782
+ }
1647
1783
 
1648
1784
  const created = createClient(model, {
1649
1785
  model,
@@ -1696,10 +1832,16 @@ const streamAnthropicOnce = (
1696
1832
  };
1697
1833
  let params = await prepareParams();
1698
1834
 
1835
+ // Opt-in flag: the response parser only honors `fallback` content
1836
+ // blocks and `usage.iterations` when the current request opted into
1837
+ // the server-side-fallback beta chain. Leaving `options.fallbacks`
1838
+ // unset preserves the pre-fallback stream shape on every event.
1839
+ const serverSideFallback = !!options?.fallbacks?.length;
1699
1840
  type Block = (
1700
1841
  | ThinkingContent
1701
1842
  | RedactedThinkingContent
1702
1843
  | TextContent
1844
+ | AnthropicFallbackContent
1703
1845
  | (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number })
1704
1846
  ) & { [kStreamingBlockIndex]: number };
1705
1847
  const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
@@ -1815,7 +1957,10 @@ const streamAnthropicOnce = (
1815
1957
  const closedBlockIndexes = new Set<number>();
1816
1958
  const openBlocks = new Map<
1817
1959
  number,
1818
- { contentIndex: number; kind: "text" | "thinking" | "redactedThinking" | "toolCall" | "ignored" }
1960
+ {
1961
+ contentIndex: number;
1962
+ kind: "text" | "thinking" | "redactedThinking" | "fallback" | "toolCall" | "ignored";
1963
+ }
1819
1964
  >();
1820
1965
 
1821
1966
  // Pings keep the idle deadline alive once content is flowing, but a
@@ -1866,7 +2011,15 @@ const streamAnthropicOnce = (
1866
2011
  output.usage.cacheWrite = startUsage.cache_creation_input_tokens || 0;
1867
2012
  output.usage.totalTokens =
1868
2013
  output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
1869
- calculateCost(model, output.usage);
2014
+ if (serverSideFallback) {
2015
+ const served = fallbackServedModelFromUsage(startUsage);
2016
+ if (served) output.model = served;
2017
+ if (!calculateFallbackTurnCost(model, output.usage, startUsage)) {
2018
+ calculateCost(model, output.usage);
2019
+ }
2020
+ } else {
2021
+ calculateCost(model, output.usage);
2022
+ }
1870
2023
  } else {
1871
2024
  reportAnthropicEnvelopeAnomaly("message_start missing usage");
1872
2025
  }
@@ -1904,6 +2057,33 @@ const streamAnthropicOnce = (
1904
2057
  continue;
1905
2058
  }
1906
2059
  if (!firstTokenTime) firstTokenTime = performance.now();
2060
+ if (event.content_block.type === "fallback") {
2061
+ // Fallback boundary is only meaningful when the request
2062
+ // opted into the beta chain — silently drop otherwise so
2063
+ // unopted-in sessions never see the block persisted or
2064
+ // influence downstream converters.
2065
+ const fallback = parseAnthropicFallbackWireBlock(event.content_block);
2066
+ if (!serverSideFallback || !fallback) {
2067
+ if (!fallback) {
2068
+ reportAnthropicEnvelopeAnomaly("fallback content_block missing model refs");
2069
+ }
2070
+ openBlocks.set(event.index, { contentIndex: -1, kind: "ignored" });
2071
+ continue;
2072
+ }
2073
+ const block: Block = { ...fallback, [kStreamingBlockIndex]: event.index };
2074
+ output.content.push(block);
2075
+ openBlocks.set(event.index, {
2076
+ contentIndex: output.content.length - 1,
2077
+ kind: "fallback",
2078
+ });
2079
+ // A fallback content block is the mid-stream signal that a
2080
+ // classifier block on the primary was retried on the
2081
+ // fallback model. Adopt the served id immediately so
2082
+ // pricing decisions downstream (final usage.iterations may
2083
+ // arrive before/after) see the right model.
2084
+ output.model = fallback.to.model;
2085
+ continue;
2086
+ }
1907
2087
  if (event.content_block.type === "text") {
1908
2088
  streamedReplayUnsafeContent = true;
1909
2089
  const block: Block = {
@@ -2119,7 +2299,15 @@ const streamAnthropicOnce = (
2119
2299
  applyAnthropicUsageExtras(output.usage, deltaUsage);
2120
2300
  output.usage.totalTokens =
2121
2301
  output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
2122
- calculateCost(model, output.usage);
2302
+ if (serverSideFallback) {
2303
+ const served = fallbackServedModelFromUsage(deltaUsage);
2304
+ if (served) output.model = served;
2305
+ if (!calculateFallbackTurnCost(model, output.usage, deltaUsage)) {
2306
+ calculateCost(model, output.usage);
2307
+ }
2308
+ } else {
2309
+ calculateCost(model, output.usage);
2310
+ }
2123
2311
  }
2124
2312
  } else if (event.type === "message_stop") {
2125
2313
  sawTerminalEnvelope = true;
@@ -2180,6 +2368,7 @@ const streamAnthropicOnce = (
2180
2368
  params = await prepareParams();
2181
2369
  providerRetryAttempt = 0;
2182
2370
  output.content.length = 0;
2371
+ output.model = model.id;
2183
2372
  output.responseId = undefined;
2184
2373
  output.errorMessage = undefined;
2185
2374
  output.providerPayload = undefined;
@@ -2206,6 +2395,7 @@ const streamAnthropicOnce = (
2206
2395
  params = await prepareParams();
2207
2396
  providerRetryAttempt = 0;
2208
2397
  output.content.length = 0;
2398
+ output.model = model.id;
2209
2399
  output.responseId = undefined;
2210
2400
  output.errorMessage = undefined;
2211
2401
  output.providerPayload = undefined;
@@ -2248,6 +2438,7 @@ const streamAnthropicOnce = (
2248
2438
  await scheduler.wait(delayMs, { signal: options?.signal });
2249
2439
  }
2250
2440
  output.content.length = 0;
2441
+ output.model = model.id;
2251
2442
  output.responseId = undefined;
2252
2443
  output.errorMessage = undefined;
2253
2444
  output.stopDetails = undefined;
@@ -2960,7 +3151,9 @@ function buildParams(
2960
3151
  // metadata → max_tokens → thinking → context_management → output_config → stream.
2961
3152
  const params: MessageCreateParamsStreaming = {
2962
3153
  model: options?.requestModelId ?? model.requestModelId ?? model.id,
2963
- messages: convertAnthropicMessages(context.messages, model, isOAuthToken),
3154
+ messages: convertAnthropicMessages(context.messages, model, isOAuthToken, {
3155
+ serverSideFallbackEnabled: !!options?.fallbacks?.length,
3156
+ }),
2964
3157
  ...(systemBlocks && { system: systemBlocks }),
2965
3158
  ...(tools !== undefined && { tools }),
2966
3159
  ...(metadata && { metadata }),
@@ -2968,6 +3161,7 @@ function buildParams(
2968
3161
  ...(thinking && { thinking }),
2969
3162
  ...(contextManagement && { context_management: contextManagement }),
2970
3163
  ...(outputConfig && { output_config: outputConfig }),
3164
+ ...(options?.fallbacks?.length ? { fallbacks: options.fallbacks } : {}),
2971
3165
  stream: true,
2972
3166
  };
2973
3167
 
@@ -3121,10 +3315,20 @@ function toWellFormedDeep(value: unknown): unknown {
3121
3315
  return value;
3122
3316
  }
3123
3317
 
3318
+ /**
3319
+ * Serialize omp {@link Message}s to Anthropic wire messages.
3320
+ *
3321
+ * `opts.serverSideFallbackEnabled` — when the CURRENT request itself
3322
+ * opts into the server-side-fallback beta chain. Only then may a persisted
3323
+ * `fallback` content block from a prior turn be replayed on the wire;
3324
+ * otherwise the block is dropped to avoid a 400 on non-fallback requests
3325
+ * that don't send the beta.
3326
+ */
3124
3327
  export function convertAnthropicMessages(
3125
3328
  messages: Message[],
3126
3329
  model: Model<"anthropic-messages">,
3127
3330
  isOAuthToken: boolean,
3331
+ opts?: { serverSideFallbackEnabled?: boolean },
3128
3332
  ): AnthropicMessageParam[] {
3129
3333
  // Indices of params emitted from `developer` messages. After the main pass,
3130
3334
  // the ones whose placement satisfies Anthropic's mid-conversation rules are
@@ -3214,6 +3418,19 @@ export function convertAnthropicMessages(
3214
3418
  type: "redacted_thinking",
3215
3419
  data: block.data,
3216
3420
  });
3421
+ } else if (block.type === "fallback") {
3422
+ // Replay ONLY when both sides are aligned: the current
3423
+ // request opted into the beta chain, and the target is
3424
+ // official Anthropic (the only endpoint that accepts the
3425
+ // block on the wire). `transformMessages` already drops
3426
+ // the block for cross-provider / non-official replays, so
3427
+ // this is defense-in-depth for direct convert calls.
3428
+ if (!opts?.serverSideFallbackEnabled || !model.compat.officialEndpoint) continue;
3429
+ blocks.push({
3430
+ type: "fallback",
3431
+ from: block.from,
3432
+ to: block.to,
3433
+ });
3217
3434
  } else if (block.type === "toolCall") {
3218
3435
  blocks.push({
3219
3436
  type: "tool_use",
@@ -340,6 +340,7 @@ function buildParams(
340
340
  systemRole,
341
341
  includeThinkingSignatures: true,
342
342
  developerStringContent: true,
343
+ preserveAssistantMessageIds: true,
343
344
  });
344
345
 
345
346
  const params: AzureOpenAIResponsesSamplingParams = {
@@ -375,7 +376,7 @@ function buildParams(
375
376
  }
376
377
  }
377
378
 
378
- applyResponsesReasoningParams(params, model, options, messages);
379
+ applyResponsesReasoningParams(params, model, options);
379
380
 
380
381
  return params;
381
382
  }
@@ -166,8 +166,8 @@ interface OpenAIResponsesProviderSessionState
166
166
 
167
167
  interface OpenAIResponsesChainState {
168
168
  /**
169
- * Wire params of the last successful turn, with per-turn trailing
170
- * scaffolding stripped from `input` (never carries previous_response_id).
169
+ * Wire params of the last successful turn; never carries
170
+ * `previous_response_id`.
171
171
  */
172
172
  lastParams?: OpenAIResponsesSamplingParams;
173
173
  lastResponseId?: string;
@@ -256,30 +256,19 @@ interface OpenAIResponsesChainedParams {
256
256
  * (same options, strict history prefix), chain via `previous_response_id` +
257
257
  * delta-only `input`; otherwise break the chain and replay the full transcript.
258
258
  *
259
- * The prefix check runs on the wire form of the conversation arguments alone:
260
- * per-turn trailing scaffolding is excluded from both sides and re-appended to
261
- * the delta, so a decoration that trails every request can never masquerade as
262
- * a history mutation.
259
+ * The prefix check runs on the wire form of the conversation arguments, so
260
+ * history mutations or option changes force a full replay.
263
261
  */
264
262
  function buildOpenAIResponsesChainedParams(
265
263
  params: OpenAIResponsesSamplingParams,
266
- trailingScaffoldingItems: number,
267
264
  chain: OpenAIResponsesChainState,
268
265
  ): OpenAIResponsesChainedParams {
269
- const historyParams =
270
- trailingScaffoldingItems > 0 && Array.isArray(params.input)
271
- ? { ...params, input: params.input.slice(0, params.input.length - trailingScaffoldingItems) }
272
- : params;
273
266
  const deltaInput = chain.canAppend
274
- ? buildResponsesDeltaInput(chain.lastParams, chain.lastResponseItems, historyParams)
267
+ ? buildResponsesDeltaInput(chain.lastParams, chain.lastResponseItems, params)
275
268
  : null;
276
269
  if (deltaInput && deltaInput.length > 0 && chain.lastResponseId) {
277
- const scaffolding =
278
- historyParams !== params && Array.isArray(params.input)
279
- ? params.input.slice(params.input.length - trailingScaffoldingItems)
280
- : [];
281
270
  return {
282
- params: { ...params, previous_response_id: chain.lastResponseId, input: [...deltaInput, ...scaffolding] },
271
+ params: { ...params, previous_response_id: chain.lastResponseId, input: deltaInput },
283
272
  previousResponseId: chain.lastResponseId,
284
273
  };
285
274
  }
@@ -416,9 +405,7 @@ const streamOpenAIResponsesOnce = (
416
405
  const strictToolsScope = getOpenAIStrictToolsScope(model, baseUrl);
417
406
  const builtParams = buildParams(model, context, options, providerSessionState, strictToolsScope);
418
407
  const params = builtParams.params;
419
- const { trailingScaffoldingItems } = builtParams;
420
408
  let activeParams = params;
421
- let activeTrailingScaffoldingItems = trailingScaffoldingItems;
422
409
  const resolvedBaseUrl = (baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "");
423
410
  const requestReasoningEffortFallbacks = new Map<string, OpenAIReasoningEffortFallback>();
424
411
  const attemptedReasoningEffortFallbacks = new Set<string>();
@@ -448,9 +435,7 @@ const streamOpenAIResponsesOnce = (
448
435
  }
449
436
  applyReasoningEffortFallbackForRequest(params);
450
437
  let chained: OpenAIResponsesChainedParams =
451
- chainState && !chainState.disabled
452
- ? buildOpenAIResponsesChainedParams(params, trailingScaffoldingItems, chainState)
453
- : { params };
438
+ chainState && !chainState.disabled ? buildOpenAIResponsesChainedParams(params, chainState) : { params };
454
439
  sentPreviousResponseId = chained.previousResponseId;
455
440
  const idleTimeoutMs =
456
441
  options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.compat.streamIdleTimeoutMs);
@@ -589,11 +574,7 @@ const streamOpenAIResponsesOnce = (
589
574
  if (chainState && !chainState.disabled) fallbackParams.store = true;
590
575
  let fallbackChained: OpenAIResponsesChainedParams =
591
576
  chainState && !chainState.disabled
592
- ? buildOpenAIResponsesChainedParams(
593
- fallbackParams,
594
- fallbackBuilt.trailingScaffoldingItems,
595
- chainState,
596
- )
577
+ ? buildOpenAIResponsesChainedParams(fallbackParams, chainState)
597
578
  : { params: fallbackParams };
598
579
  sentPreviousResponseId = fallbackChained.previousResponseId;
599
580
  fallbackChained = {
@@ -603,7 +584,6 @@ const streamOpenAIResponsesOnce = (
603
584
  chained = fallbackChained;
604
585
  rawRequestDump.body = chained.params;
605
586
  activeParams = fallbackParams;
606
- activeTrailingScaffoldingItems = fallbackBuilt.trailingScaffoldingItems;
607
587
  activeStrictToolsApplied = fallbackBuilt.strictToolsApplied;
608
588
  continue;
609
589
  }
@@ -646,7 +626,6 @@ const streamOpenAIResponsesOnce = (
646
626
  chained = { params: retryParams };
647
627
  rawRequestDump.body = retryParams;
648
628
  activeParams = currentParams;
649
- activeTrailingScaffoldingItems = currentBuilt.trailingScaffoldingItems;
650
629
  activeStrictToolsApplied = currentBuilt.strictToolsApplied;
651
630
  }
652
631
  }
@@ -710,14 +689,7 @@ const streamOpenAIResponsesOnce = (
710
689
  output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
711
690
  if (providerSessionState) providerSessionState.nativeHistoryReplayWarmed = true;
712
691
  if (chainState) {
713
- chainState.lastParams = structuredCloneJSON(
714
- activeTrailingScaffoldingItems > 0 && Array.isArray(activeParams.input)
715
- ? {
716
- ...activeParams,
717
- input: activeParams.input.slice(0, activeParams.input.length - activeTrailingScaffoldingItems),
718
- }
719
- : activeParams,
720
- );
692
+ chainState.lastParams = structuredCloneJSON(activeParams);
721
693
  if (output.responseId) {
722
694
  chainState.lastResponseId = output.responseId;
723
695
  chainState.lastResponseItems = sanitizeOpenAIResponsesHistoryItemsForReplay(
@@ -790,7 +762,7 @@ export function buildParams(
790
762
  providerSessionState: OpenAIResponsesProviderSessionState | undefined,
791
763
  strictToolsScope?: OpenAIStrictToolsScope,
792
764
  disableStrictToolsOverride = false,
793
- ): { params: OpenAIResponsesSamplingParams; trailingScaffoldingItems: number; strictToolsApplied: boolean } {
765
+ ): { params: OpenAIResponsesSamplingParams; strictToolsApplied: boolean } {
794
766
  const policy = resolveOpenAICompatPolicy(model, {
795
767
  endpoint: "responses",
796
768
  reasoning: options?.reasoning,
@@ -914,7 +886,7 @@ export function buildParams(
914
886
  filterReasoningHistory: options?.filterReasoningHistory,
915
887
  omitReasoningEffort: options?.omitReasoningEffort,
916
888
  });
917
- const trailingScaffoldingItems = applyResponsesCompatPolicy(params, messages, reasoningPolicy, {
889
+ applyResponsesCompatPolicy(params, reasoningPolicy, {
918
890
  reasoningSummary: options?.reasoningSummary,
919
891
  mapEffort: effort =>
920
892
  model.compat.reasoningEffortMap?.[effort as NonNullable<OpenAIResponsesOptions["reasoning"]>] ??
@@ -926,7 +898,7 @@ export function buildParams(
926
898
 
927
899
  applyOpenAIExtraBody(params, options?.extraBody);
928
900
 
929
- return { params, trailingScaffoldingItems, strictToolsApplied };
901
+ return { params, strictToolsApplied };
930
902
  }
931
903
 
932
904
  /**