@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.
- package/CHANGELOG.md +20 -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-shared.d.ts +3 -1
- 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 +1 -0
- package/src/providers/openai-responses-reasoning-suppression.md +1 -0
- package/src/providers/openai-shared.ts +64 -23
- 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",
|
|
@@ -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
|
-
|
|
1475
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
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
|
|
1503
|
-
}
|
|
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 (
|
|
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.
|
|
2450
|
+
if (policy.compat.requiresReasoningSuppressionPrompt && reasoning.requestedEffort === undefined) {
|
|
2410
2451
|
messages.push({
|
|
2411
2452
|
role: "developer",
|
|
2412
|
-
content: [{ type: "input_text", text:
|
|
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.
|
|
2482
|
+
if (policy.compat.requiresReasoningSuppressionPrompt) {
|
|
2442
2483
|
messages.push({
|
|
2443
2484
|
role: "developer",
|
|
2444
|
-
content: [{ type: "input_text", text:
|
|
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
|
|
9
|
-
"
|
|
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
|
-
|
|
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
|
|
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";
|