@oh-my-pi/pi-ai 16.2.13 → 16.3.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.
@@ -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 = {
@@ -0,0 +1 @@
1
+ Keep internal reasoning brief. Continue following the task and use tools normally.
@@ -17,6 +17,7 @@ import {
17
17
  COREWEAVE_PROJECT_HEADER,
18
18
  coreWeaveProjectHeaders,
19
19
  hasCoreWeaveProjectHeader,
20
+ removeBlankCoreWeaveProjectHeaders,
20
21
  } from "@oh-my-pi/pi-catalog/wire/coreweave";
21
22
  import { parseGitHubCopilotApiKey } from "@oh-my-pi/pi-catalog/wire/github-copilot";
22
23
  import {
@@ -75,6 +76,7 @@ import {
75
76
  } from "./github-copilot-headers";
76
77
  import type { ChatCompletionCreateParamsStreaming } from "./openai-chat-wire";
77
78
  import type { InputItem } from "./openai-codex/request-transformer";
79
+ import responsesReasoningSuppressionPrompt from "./openai-responses-reasoning-suppression.md" with { type: "text" };
78
80
  import type {
79
81
  ResponseContentPartAddedEvent,
80
82
  ResponseCreateParamsStreaming,
@@ -160,6 +162,7 @@ function resolveSakanaRequestBaseUrl(): string | undefined {
160
162
  }
161
163
 
162
164
  function applyCoreWeaveProjectHeader(headers: Record<string, string>): void {
165
+ removeBlankCoreWeaveProjectHeaders(headers);
163
166
  if (hasCoreWeaveProjectHeader(headers)) {
164
167
  return;
165
168
  }
@@ -1344,6 +1347,8 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
1344
1347
  includeThinkingSignatures?: boolean;
1345
1348
  developerStringContent?: boolean;
1346
1349
  repairOrphanOutputs?: boolean;
1350
+ /** Preserve assistant message item IDs from text signatures during fallback replay. */
1351
+ preserveAssistantMessageIds?: boolean;
1347
1352
  }
1348
1353
 
1349
1354
  export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput {
@@ -1432,6 +1437,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1432
1437
  knownCallIds,
1433
1438
  includeThinkingSignatures,
1434
1439
  customCallIds,
1440
+ options.preserveAssistantMessageIds,
1435
1441
  );
1436
1442
  if (outputItems.length === 0) continue;
1437
1443
  messages.push(...outputItems);
@@ -1453,6 +1459,21 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1453
1459
  return repairOrphanResponsesToolCalls(withRepairedOutputs);
1454
1460
  }
1455
1461
 
1462
+ type ResponsesReplayAssistantMessage = Omit<ResponseOutputMessage, "id"> & { id?: string };
1463
+
1464
+ function parseResponseReasoningReplayItem(signature: string | undefined): ResponseReasoningItem | undefined {
1465
+ if (!signature) return undefined;
1466
+ try {
1467
+ const parsed = JSON.parse(signature) as unknown;
1468
+ if (!parsed || typeof parsed !== "object") return undefined;
1469
+ if (!("type" in parsed) || parsed.type !== "reasoning") return undefined;
1470
+ if (!("id" in parsed) || typeof parsed.id !== "string") return undefined;
1471
+ return parsed as ResponseReasoningItem;
1472
+ } catch {
1473
+ return undefined;
1474
+ }
1475
+ }
1476
+
1456
1477
  export function convertResponsesAssistantMessage<TApi extends Api>(
1457
1478
  assistantMsg: AssistantMessage,
1458
1479
  model: Model<TApi>,
@@ -1460,9 +1481,16 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
1460
1481
  knownCallIds: Set<string>,
1461
1482
  includeThinkingSignatures = true,
1462
1483
  customCallIds?: Set<string>,
1484
+ preserveMessageIds = false,
1463
1485
  ): ResponseInput {
1464
1486
  const outputItems: ResponseInput = [];
1465
1487
  let unsignedTextBlocks = 0;
1488
+ const hasReplayableReasoningItem =
1489
+ includeThinkingSignatures &&
1490
+ assistantMsg.stopReason !== "error" &&
1491
+ assistantMsg.content.some(
1492
+ block => block.type === "thinking" && parseResponseReasoningReplayItem(block.thinkingSignature) !== undefined,
1493
+ );
1466
1494
  const isDifferentModel =
1467
1495
  assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
1468
1496
 
@@ -1471,14 +1499,8 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
1471
1499
  if (!includeThinkingSignatures) {
1472
1500
  continue;
1473
1501
  }
1474
- if (block.thinkingSignature) {
1475
- try {
1476
- outputItems.push(JSON.parse(block.thinkingSignature) as ResponseReasoningItem);
1477
- } catch {
1478
- // Legacy/corrupt persisted signature — skip the reasoning item
1479
- // rather than failing the whole request build.
1480
- }
1481
- }
1502
+ const reasoningItem = parseResponseReasoningReplayItem(block.thinkingSignature);
1503
+ if (reasoningItem) outputItems.push(reasoningItem);
1482
1504
  continue;
1483
1505
  }
1484
1506
 
@@ -1486,21 +1508,30 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
1486
1508
  const parsedSignature = parseTextSignature(block.textSignature);
1487
1509
  let msgId = parsedSignature?.id;
1488
1510
  if (!msgId) {
1489
- // Distinct ids per unsigned block: several text blocks in one message
1490
- // (cross-provider replay downgrades thinking text) must not share an id.
1491
- msgId = unsignedTextBlocks === 0 ? `msg_${msgIndex}` : `msg_${msgIndex}_${unsignedTextBlocks}`;
1492
- unsignedTextBlocks += 1;
1511
+ if (hasReplayableReasoningItem) {
1512
+ // Distinct ids per unsigned block: several text blocks in one message
1513
+ // (cross-provider replay downgrades thinking text) must not share an id.
1514
+ msgId = unsignedTextBlocks === 0 ? `msg_${msgIndex}` : `msg_${msgIndex}_${unsignedTextBlocks}`;
1515
+ unsignedTextBlocks += 1;
1516
+ }
1517
+ } else if (!preserveMessageIds && !hasReplayableReasoningItem) {
1518
+ // Without the matching reasoning item the server rejects replayed
1519
+ // item ids (#4173) — drop them regardless of shape, including
1520
+ // legacy plain-string signatures that would otherwise fall into
1521
+ // the >64-char hash branch and fabricate a bogus msg_ id.
1522
+ msgId = undefined;
1493
1523
  } else if (msgId.length > 64) {
1494
1524
  msgId = `msg_${Bun.hash(msgId).toString(36)}`;
1495
1525
  }
1496
- outputItems.push({
1526
+ const messageItem: ResponsesReplayAssistantMessage = {
1497
1527
  type: "message",
1498
1528
  role: "assistant",
1499
1529
  content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
1500
1530
  status: "completed",
1501
- id: msgId,
1502
- phase: parsedSignature?.phase,
1503
- } satisfies ResponseOutputMessage);
1531
+ ...(msgId ? { id: msgId } : {}),
1532
+ ...(parsedSignature?.phase ? { phase: parsedSignature.phase } : {}),
1533
+ };
1534
+ outputItems.push(messageItem as ResponseInput[number]);
1504
1535
  continue;
1505
1536
  }
1506
1537
 
@@ -1510,7 +1541,15 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
1510
1541
 
1511
1542
  const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
1512
1543
  let itemId: string | undefined = normalized.itemId;
1513
- if (isDifferentModel && (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))) {
1544
+ if (
1545
+ !hasReplayableReasoningItem &&
1546
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
1547
+ ) {
1548
+ itemId = undefined;
1549
+ } else if (
1550
+ isDifferentModel &&
1551
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
1552
+ ) {
1514
1553
  itemId = undefined;
1515
1554
  }
1516
1555
  knownCallIds.add(normalized.callId);
@@ -1519,7 +1558,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
1519
1558
  customCallIds?.add(normalized.callId);
1520
1559
  outputItems.push({
1521
1560
  type: "custom_tool_call",
1522
- id: itemId,
1561
+ ...(itemId ? { id: itemId } : {}),
1523
1562
  call_id: normalized.callId,
1524
1563
  name: block.customWireName,
1525
1564
  input: rawInput,
@@ -1528,7 +1567,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
1528
1567
  }
1529
1568
  outputItems.push({
1530
1569
  type: "function_call",
1531
- id: itemId,
1570
+ ...(itemId ? { id: itemId } : {}),
1532
1571
  call_id: normalized.callId,
1533
1572
  name: block.name,
1534
1573
  arguments: JSON.stringify(block.arguments),
@@ -2365,6 +2404,8 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
2365
2404
  applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
2366
2405
  }
2367
2406
 
2407
+ const RESPONSES_REASONING_SUPPRESSION_PROMPT = responsesReasoningSuppressionPrompt.trim();
2408
+
2368
2409
  type ReasoningOptions = {
2369
2410
  reasoning?: string;
2370
2411
  reasoningSummary?: "auto" | "detailed" | "concise" | null;
@@ -2406,10 +2447,10 @@ export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreami
2406
2447
  ReasoningParam;
2407
2448
  return 0;
2408
2449
  }
2409
- if (policy.compat.requiresJuiceZeroHack && reasoning.requestedEffort === undefined) {
2450
+ if (policy.compat.requiresReasoningSuppressionPrompt && reasoning.requestedEffort === undefined) {
2410
2451
  messages.push({
2411
2452
  role: "developer",
2412
- content: [{ type: "input_text", text: "# Juice: 0 !important" }],
2453
+ content: [{ type: "input_text", text: RESPONSES_REASONING_SUPPRESSION_PROMPT }],
2413
2454
  });
2414
2455
  return 1;
2415
2456
  }
@@ -2438,10 +2479,10 @@ export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreami
2438
2479
  return 0;
2439
2480
  }
2440
2481
 
2441
- if (policy.compat.requiresJuiceZeroHack) {
2482
+ if (policy.compat.requiresReasoningSuppressionPrompt) {
2442
2483
  messages.push({
2443
2484
  role: "developer",
2444
- content: [{ type: "input_text", text: "# Juice: 0 !important" }],
2485
+ content: [{ type: "input_text", text: RESPONSES_REASONING_SUPPRESSION_PROMPT }],
2445
2486
  });
2446
2487
  return 1;
2447
2488
  }
@@ -431,6 +431,16 @@ export function transformMessages<TApi extends Api>(
431
431
  if (!sanitized.thinkingSignature && (!sanitized.thinking || sanitized.thinking.trim() === "")) {
432
432
  return [];
433
433
  }
434
+ // Same-model Anthropic replay to a signature-enforcing endpoint
435
+ // cannot natively replay thinking blocks whose source explicitly
436
+ // recorded an empty signature, but this is not a dialect
437
+ // transition. Do not demote that sentinel into the target model's
438
+ // textual thinking dialect; keep demotion for signatures stripped
439
+ // by the untrustworthy-turn recovery above and for literal thinking
440
+ // envelopes that never carried a signature field.
441
+ if (isSameModel && isOfficialAnthropicTarget && sanitized.thinkingSignature?.trim() === "") {
442
+ return [];
443
+ }
434
444
  return sanitized;
435
445
  }
436
446
  // Cross-API target: same-model replay keeps signatures untouched
@@ -481,6 +491,22 @@ export function transformMessages<TApi extends Api>(
481
491
  return [];
482
492
  }
483
493
 
494
+ if (block.type === "fallback") {
495
+ // Server-side-fallback boundary marker (Anthropic beta
496
+ // `server-side-fallback-2026-06-01`). Only the official
497
+ // Anthropic endpoint accepts this block on replay: every
498
+ // other target either rejects unknown content blocks with a
499
+ // 400 (anthropic-compatible endpoints like Umans/Z.AI/MiniMax,
500
+ // and older omp gateways whose schema pre-dates this feature)
501
+ // or throws in its converter (Bedrock). Even the official
502
+ // replay path only accepts the block when the current request
503
+ // itself opts into the beta — but we don't know that here, so
504
+ // keep it and let `convertAnthropicMessages` re-check the
505
+ // per-request opt-in before serializing.
506
+ if (isAnthropicTarget && model.compat.officialEndpoint) return block;
507
+ return [];
508
+ }
509
+
484
510
  if (block.type === "text") {
485
511
  if (isSameModel) return block;
486
512
  return {
@@ -5,14 +5,15 @@ import { createApiKeyLogin } from "./api-key-login";
5
5
  import type { OAuthLoginCallbacks } from "./oauth/types";
6
6
  import type { ProviderDefinition } from "./types";
7
7
 
8
- const PROJECT_SETUP_INSTRUCTIONS =
9
- "Create or select a CoreWeave Serverless Inference project, set COREWEAVE_PROJECT=<team>/<project> for the OpenAI-Project header, then copy your API key from account settings";
8
+ const PROJECT_PERSIST_INSTRUCTIONS =
9
+ "add export COREWEAVE_PROJECT=<team>/<project> to your shell startup file (for example ~/.zshrc, ~/.bashrc, or your shell's profile/rc file)";
10
+ const PROJECT_SETUP_INSTRUCTIONS = `Create or select a CoreWeave Serverless Inference project, ${PROJECT_PERSIST_INSTRUCTIONS} for the OpenAI-Project header, then copy your API key from account settings`;
10
11
 
11
12
  function requireCoreWeaveProjectHeaders(): Record<string, string> {
12
13
  const headers = coreWeaveProjectHeaders($env);
13
14
  if (!headers) {
14
15
  throw new AIError.ConfigurationError(
15
- "CoreWeave Serverless Inference requires OpenAI-Project. Set COREWEAVE_PROJECT=<team>/<project> before running /login coreweave.",
16
+ `CoreWeave Serverless Inference requires OpenAI-Project. Set COREWEAVE_PROJECT=<team>/<project> before running /login coreweave. To persist it, ${PROJECT_PERSIST_INSTRUCTIONS}.`,
16
17
  );
17
18
  }
18
19
  return headers;
@@ -18,7 +18,7 @@ const STANDARD_AUTH_URL = "https://platform.xiaomimimo.com/#/console/api-keys";
18
18
  const TOKEN_PLAN_AUTH_URL = "https://platform.xiaomimimo.com/console/plan-manage";
19
19
  const STANDARD_API_BASE_URL = "https://api.xiaomimimo.com/v1";
20
20
  const TOKEN_PLAN_KEY_PREFIX = "tp-";
21
- const STANDARD_VALIDATION_MODEL = "mimo-v2-flash";
21
+ const STANDARD_VALIDATION_MODEL = "mimo-v2.5";
22
22
  const TOKEN_PLAN_VALIDATION_MODEL = "mimo-v2.5";
23
23
  const TOKEN_PLAN_SGP_API_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
24
24
  const TOKEN_PLAN_AMS_API_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";