@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.
- package/CHANGELOG.md +26 -0
- package/README.md +8 -1
- package/dist/types/dialect/demotion.d.ts +8 -7
- package/dist/types/providers/anthropic-wire.d.ts +57 -1
- package/dist/types/providers/anthropic.d.ts +22 -2
- package/dist/types/providers/openai-responses.d.ts +2 -3
- package/dist/types/providers/openai-shared.d.ts +5 -3
- package/dist/types/types.d.ts +26 -2
- package/dist/types/utils/leaked-thinking-stream.d.ts +8 -4
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +11 -1
- package/src/dialect/demotion.ts +13 -8
- package/src/providers/anthropic-wire.ts +51 -2
- package/src/providers/anthropic.ts +222 -5
- package/src/providers/azure-openai-responses.ts +2 -1
- package/src/providers/openai-responses.ts +12 -40
- package/src/providers/openai-shared.ts +65 -46
- package/src/providers/transform-messages.ts +26 -0
- package/src/registry/coreweave.ts +4 -3
- package/src/registry/oauth/xiaomi.ts +1 -1
- package/src/stream.ts +69 -7
- package/src/types.ts +23 -2
- package/src/utils/leaked-thinking-stream.ts +8 -4
|
@@ -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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
170
|
-
*
|
|
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
|
|
260
|
-
*
|
|
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,
|
|
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:
|
|
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;
|
|
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
|
-
|
|
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,
|
|
901
|
+
return { params, strictToolsApplied };
|
|
930
902
|
}
|
|
931
903
|
|
|
932
904
|
/**
|