@linxiraos/pi-ai 1.2.0 → 1.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 +83 -0
- package/README.md +4 -2
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/auth/cascade.d.ts +5 -3
- package/dist/types/auth/pool.d.ts +3 -3
- package/dist/types/auth/select.d.ts +5 -1
- package/dist/types/auth/types.d.ts +15 -5
- package/dist/types/auth/usage-cache.d.ts +3 -1
- package/dist/types/auth-gateway/clients.d.ts +67 -0
- package/dist/types/auth-gateway/dispatch.d.ts +28 -10
- package/dist/types/auth-gateway/http.d.ts +2 -0
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/server.d.ts +9 -4
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/dialect/coercion.d.ts +6 -0
- package/dist/types/error/classes.d.ts +1 -1
- package/dist/types/error/flags.d.ts +15 -0
- package/dist/types/error/index.d.ts +1 -0
- package/dist/types/error/network-cause.d.ts +40 -0
- package/dist/types/images/shared.d.ts +26 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/judgment/native.d.ts +51 -3
- package/dist/types/judgment/systemone.d.ts +9 -2
- package/dist/types/judgment/text.d.ts +11 -5
- package/dist/types/judgment/types.d.ts +5 -1
- package/dist/types/judgment/wire.d.ts +16 -3
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-images.d.ts +7 -0
- package/dist/types/providers/anthropic-signature.d.ts +12 -2
- package/dist/types/providers/anthropic-state.d.ts +7 -0
- package/dist/types/providers/anthropic.d.ts +21 -6
- package/dist/types/providers/connection-prewarm.d.ts +15 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -0
- package/dist/types/providers/cursor.d.ts +7 -0
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/providers/openai-responses.d.ts +9 -0
- package/dist/types/providers/openai-shared.d.ts +30 -0
- package/dist/types/providers/systemone-server.d.ts +7 -3
- package/dist/types/providers/transform-messages.d.ts +26 -5
- package/dist/types/registry/oauth/google-antigravity.d.ts +1 -1
- package/dist/types/stream.d.ts +0 -2
- package/dist/types/types.d.ts +33 -9
- package/dist/types/utils/dsml-leak.d.ts +4 -2
- package/dist/types/utils/narration.d.ts +9 -0
- package/dist/types/utils/openai-http.d.ts +2 -0
- package/dist/types/utils/request-body.d.ts +11 -0
- package/dist/types/utils/request-compression.d.ts +21 -0
- package/dist/types/utils/tool-call-arguments.d.ts +2 -1
- package/dist/types/utils/transport-fetch.d.ts +8 -0
- package/dist/types/utils.d.ts +1 -1
- package/package.json +8 -8
- package/src/auth/cascade.ts +23 -13
- package/src/auth/pool.ts +3 -3
- package/src/auth/refresh.ts +39 -12
- package/src/auth/select.ts +8 -2
- package/src/auth/types.ts +16 -5
- package/src/auth/usage-cache.ts +23 -7
- package/src/auth/usage.ts +2 -4
- package/src/auth-broker/remote-store.ts +31 -6
- package/src/auth-gateway/clients.ts +318 -0
- package/src/auth-gateway/dispatch.ts +66 -35
- package/src/auth-gateway/http.ts +11 -5
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +4 -10
- package/src/auth-gateway/routes/images.ts +6 -10
- package/src/auth-gateway/routes/rerank.ts +4 -10
- package/src/auth-gateway/routes/speech.ts +10 -10
- package/src/auth-gateway/routes/systemone.ts +22 -12
- package/src/auth-gateway/routes/transcriptions.ts +4 -2
- package/src/auth-gateway/routes/video.ts +12 -15
- package/src/auth-gateway/server.ts +136 -53
- package/src/auth-gateway/session-state.ts +14 -0
- package/src/auth-gateway/types.ts +16 -2
- package/src/dialect/coercion.ts +16 -0
- package/src/dialect/hermes.ts +7 -11
- package/src/dialect/qwen3.ts +3 -11
- package/src/error/classes.ts +3 -2
- package/src/error/flags.ts +24 -3
- package/src/error/index.ts +1 -0
- package/src/error/network-cause.ts +179 -0
- package/src/images/codex-format-directive.md +3 -0
- package/src/images/google-antigravity.ts +2 -1
- package/src/images/google-generative-ai.ts +4 -2
- package/src/images/openai-hosted.ts +25 -2
- package/src/images/openai-images.ts +11 -6
- package/src/images/shared.ts +77 -3
- package/src/index.ts +1 -0
- package/src/judgment/chat.ts +17 -1
- package/src/judgment/native.ts +182 -12
- package/src/judgment/openai-decisions.ts +27 -6
- package/src/judgment/systemone.ts +131 -10
- package/src/judgment/text.ts +20 -7
- package/src/judgment/types.ts +5 -1
- package/src/judgment/wire.ts +17 -3
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +4 -1
- package/src/providers/anthropic-client.ts +1 -1
- package/src/providers/anthropic-images.ts +263 -0
- package/src/providers/anthropic-messages-server.ts +84 -1
- package/src/providers/anthropic-signature.ts +35 -8
- package/src/providers/anthropic-state.ts +13 -0
- package/src/providers/anthropic.ts +479 -397
- package/src/providers/connection-prewarm.ts +78 -0
- package/src/providers/cowork-fetch.ts +5 -1
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +19 -0
- package/src/providers/cursor.ts +53 -13
- package/src/providers/devin.ts +10 -4
- package/src/providers/factory-droid/gemini.ts +9 -3
- package/src/providers/google-gemini-cli.ts +11 -1
- package/src/providers/google-shared.ts +2 -6
- package/src/providers/images-server.ts +6 -1
- package/src/providers/ollama.ts +15 -5
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server.ts +29 -0
- package/src/providers/openai-codex/access-programs.ts +2 -1
- package/src/providers/openai-codex-responses.ts +49 -82
- package/src/providers/openai-completions.ts +117 -19
- package/src/providers/openai-responses.ts +25 -2
- package/src/providers/openai-shared.ts +36 -5
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +72 -12
- package/src/providers/transform-messages.ts +95 -51
- package/src/registry/oauth/google-antigravity.ts +1 -1
- package/src/stream.ts +13 -73
- package/src/types.ts +33 -9
- package/src/usage/claude.ts +2 -9
- package/src/usage/github-copilot.ts +26 -4
- package/src/usage/openai-codex.ts +13 -7
- package/src/usage/opencode-go.ts +7 -8
- package/src/usage/xai-oauth.ts +2 -9
- package/src/utils/dsml-leak.ts +30 -14
- package/src/utils/leaked-thinking-stream.ts +10 -0
- package/src/utils/narration.ts +18 -0
- package/src/utils/openai-http.ts +27 -18
- package/src/utils/request-body.ts +27 -0
- package/src/utils/request-compression.ts +202 -0
- package/src/utils/request-debug.ts +23 -9
- package/src/utils/schema/dereference.ts +51 -18
- package/src/utils/schema/draft.ts +10 -2
- package/src/utils/schema/json-schema-validator.ts +4 -10
- package/src/utils/schema/normalize.ts +6 -9
- package/src/utils/tool-call-arguments.ts +2 -1
- package/src/utils/transport-fetch.ts +37 -0
- package/src/utils/validation.ts +26 -16
- package/src/utils.ts +38 -23
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as tls from "node:tls";
|
|
3
3
|
import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@linxiraos/pi-catalog/compat/anthropic";
|
|
4
|
-
import { hostMatchesUrl, isVertexRawPredictUrl } from "@linxiraos/pi-catalog/hosts";
|
|
4
|
+
import { hostMatchesUrl, isVertexRawPredictUrl, modelMatchesHost } from "@linxiraos/pi-catalog/hosts";
|
|
5
5
|
import type { Effort } from "@linxiraos/pi-catalog/effort";
|
|
6
6
|
import { mapEffortToAnthropicAdaptiveEffort } from "@linxiraos/pi-catalog/model-thinking";
|
|
7
7
|
import { calculateCost, getBundledModel } from "@linxiraos/pi-catalog/models";
|
|
@@ -9,6 +9,7 @@ import { isAnthropicOAuthToken } from "@linxiraos/pi-catalog/utils";
|
|
|
9
9
|
import { parseGitHubCopilotApiKey } from "@linxiraos/pi-catalog/wire/github-copilot";
|
|
10
10
|
import {
|
|
11
11
|
$env,
|
|
12
|
+
$flag,
|
|
12
13
|
isEnoent,
|
|
13
14
|
logger,
|
|
14
15
|
parseJsonWithRepair,
|
|
@@ -22,11 +23,13 @@ import { parseToolCallArguments } from "../utils/tool-call-arguments";
|
|
|
22
23
|
import { waitForRetry } from "../utils/retry-wait";
|
|
23
24
|
import { getEnvApiKey } from "../env-api-key";
|
|
24
25
|
import { OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
26
|
+
import { wrapFetchForRequestCompression } from "../utils/request-compression";
|
|
25
27
|
import type {
|
|
26
28
|
AnthropicCompactionFiles,
|
|
27
29
|
AnthropicCompactionPayload,
|
|
28
30
|
AnthropicCompactionRequest,
|
|
29
31
|
AnthropicFallbackContent,
|
|
32
|
+
AnthropicFallbackCreditHandle,
|
|
30
33
|
AnthropicMessagePayload,
|
|
31
34
|
AnthropicOutputEffort,
|
|
32
35
|
AnthropicRequestControls,
|
|
@@ -116,10 +119,12 @@ import {
|
|
|
116
119
|
type MessageCreateParams,
|
|
117
120
|
type MessageCreateParamsStreaming,
|
|
118
121
|
type MessageParam,
|
|
122
|
+
type OutputConfig,
|
|
119
123
|
parseAnthropicInputTransformations,
|
|
120
124
|
type RawMessageStreamEvent,
|
|
121
125
|
THINKING_BINDING_CONTROLS_BETA,
|
|
122
126
|
type TextBlockParam,
|
|
127
|
+
type ThinkingConfigParam,
|
|
123
128
|
} from "./anthropic-wire";
|
|
124
129
|
import {
|
|
125
130
|
claudeCodeSdkVersion,
|
|
@@ -138,10 +143,11 @@ import {
|
|
|
138
143
|
resolveGitHubCopilotBaseUrl,
|
|
139
144
|
wrapFetchForCopilotFallback,
|
|
140
145
|
} from "./github-copilot-headers";
|
|
141
|
-
import {
|
|
146
|
+
import { prepareAnthropicImageContext } from "./anthropic-images";
|
|
147
|
+
import { isAnthropicNarrationSignature, servedModelFromAnthropicSignature } from "./anthropic-signature";
|
|
142
148
|
import { getOpenAIPromptCacheKey } from "./openai-shared";
|
|
143
149
|
import { applyInferenceHeaders } from "./inference-headers";
|
|
144
|
-
import { redactSensitiveCredentials, transformMessages } from "./transform-messages";
|
|
150
|
+
import { isAnthropicMessagesModel, redactSensitiveCredentials, transformMessages } from "./transform-messages";
|
|
145
151
|
import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
|
|
146
152
|
import {
|
|
147
153
|
injectedClientBaseUrl,
|
|
@@ -235,6 +241,12 @@ const contextManagementBeta = "context-management-2025-06-27";
|
|
|
235
241
|
const structuredOutputsBeta = "structured-outputs-2025-12-15";
|
|
236
242
|
const thinkingTokenCountBeta = "thinking-token-count-2026-05-13";
|
|
237
243
|
const fallbackCreditBeta = "fallback-credit-2026-06-01";
|
|
244
|
+
/**
|
|
245
|
+
* Redemption window of a refusal's `fallback_credit_token`. `stop_details`
|
|
246
|
+
* reports no expiry; the documented window is five minutes from the refusal
|
|
247
|
+
* (https://platform.claude.com/docs/en/build-with-claude/fallback-credit).
|
|
248
|
+
*/
|
|
249
|
+
const FALLBACK_CREDIT_TTL_MS = 5 * 60 * 1000;
|
|
238
250
|
const promptCachingScopeBeta = "prompt-caching-scope-2026-01-05";
|
|
239
251
|
const claudeCodeUtilityBetaDefaults = [
|
|
240
252
|
oauthAuthBeta,
|
|
@@ -273,24 +285,41 @@ const promptCacheBetas: Record<string, true> = {
|
|
|
273
285
|
};
|
|
274
286
|
|
|
275
287
|
/**
|
|
276
|
-
*
|
|
288
|
+
* Betas withheld under the kill switch: they gate the pre-release body fields
|
|
289
|
+
* it removes (Claude Code's gateway contract: context management, beta tool
|
|
290
|
+
* fields, structured-output format, task budgets, tool search), plus the
|
|
291
|
+
* thinking-binding and compaction controls omp also withholds.
|
|
292
|
+
*/
|
|
293
|
+
const killSwitchBetas: Record<string, true> = {
|
|
294
|
+
[THINKING_BINDING_CONTROLS_BETA]: true,
|
|
295
|
+
[contextManagementBeta]: true,
|
|
296
|
+
[COMPACTION_BETA]: true,
|
|
297
|
+
[LEGACY_COMPACTION_BETA]: true,
|
|
298
|
+
[structuredOutputsBeta]: true,
|
|
299
|
+
[taskBudgetBeta]: true,
|
|
300
|
+
[midConversationToolChangesBeta]: true,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Remove the `dropped` betas from every `anthropic-beta` entry in a header
|
|
277
305
|
* record. Header names are case-insensitive and the value is a comma-separated
|
|
278
306
|
* list, so each matching key keeps its own casing and its remaining tokens;
|
|
279
307
|
* an entry left with no token is deleted rather than sent empty. The input is
|
|
280
|
-
* returned untouched when it advertises
|
|
308
|
+
* returned untouched when it advertises none of them.
|
|
281
309
|
*
|
|
282
310
|
* `model.headers` and `options.headers` are caller-controlled channels that
|
|
283
311
|
* reach the wire through `buildAnthropicHeaders` (as `modelHeaders`, which an
|
|
284
312
|
* override-enabled endpoint lets replace our own beta header outright), the
|
|
285
313
|
* GitHub Copilot default headers, and the per-request beta override built for
|
|
286
|
-
* injected clients — each of which must go through here once breakpoints drop
|
|
314
|
+
* injected clients — each of which must go through here once breakpoints drop
|
|
315
|
+
* or the experimental-beta kill switch is on.
|
|
287
316
|
*/
|
|
288
|
-
function
|
|
317
|
+
function stripHeaderBetas(headers: Record<string, string>, dropped: Record<string, true>): Record<string, string> {
|
|
289
318
|
let stripped: Record<string, string> | undefined;
|
|
290
319
|
for (const key in headers) {
|
|
291
320
|
if (key.toLowerCase() !== "anthropic-beta") continue;
|
|
292
321
|
const betas = normalizeExtraBetas(headers[key]);
|
|
293
|
-
const kept = betas.filter(beta =>
|
|
322
|
+
const kept = betas.filter(beta => dropped[beta] !== true);
|
|
294
323
|
if (kept.length === betas.length) continue;
|
|
295
324
|
stripped ??= { ...headers };
|
|
296
325
|
if (kept.length === 0) {
|
|
@@ -572,7 +601,11 @@ type AnthropicProviderSessionState = ProviderSessionState & {
|
|
|
572
601
|
* Cleared on session close.
|
|
573
602
|
*/
|
|
574
603
|
cacheControlOptionsUnsupported: boolean;
|
|
575
|
-
/**
|
|
604
|
+
/**
|
|
605
|
+
* Thinking blocks the client strips after a 400 rejected their replay: a
|
|
606
|
+
* prefix-binding 400 that forced a retry without `drop_block`, or a latest
|
|
607
|
+
* assistant turn whose reasoning the API refused as modified.
|
|
608
|
+
*/
|
|
576
609
|
prefixDroppedThinkingBlocks: Set<string>;
|
|
577
610
|
/**
|
|
578
611
|
* Server-reported `prefix_binding_mismatch` drops already warned about. A
|
|
@@ -919,15 +952,11 @@ function getUmansWebSearchProvider(headers: Record<string, string> | undefined):
|
|
|
919
952
|
return normalizeUmansWebSearchProvider($env.UMANS_WEBSEARCH_PROVIDER);
|
|
920
953
|
}
|
|
921
954
|
|
|
922
|
-
function isUmansAnthropicModel(model: Model<"anthropic-messages">): boolean {
|
|
923
|
-
return model.provider === "umans" || model.baseUrl.toLowerCase().includes("api.code.umans.ai");
|
|
924
|
-
}
|
|
925
|
-
|
|
926
955
|
function getUmansWebSearchHeader(
|
|
927
956
|
model: Model<"anthropic-messages">,
|
|
928
957
|
headers: Record<string, string> | undefined,
|
|
929
958
|
): Record<string, string> | undefined {
|
|
930
|
-
if (!
|
|
959
|
+
if (!modelMatchesHost(model, "umans")) return undefined;
|
|
931
960
|
const provider = getUmansWebSearchProvider(headers);
|
|
932
961
|
return provider ? { [UMANS_WEBSEARCH_PROVIDER_HEADER]: provider } : undefined;
|
|
933
962
|
}
|
|
@@ -952,253 +981,6 @@ function decodeAnthropicToolName(name: string, isOAuthToken: boolean, escapeBuil
|
|
|
952
981
|
return name;
|
|
953
982
|
}
|
|
954
983
|
|
|
955
|
-
const ANTHROPIC_MANY_IMAGE_THRESHOLD = 20;
|
|
956
|
-
const ANTHROPIC_MANY_IMAGE_MAX_DIMENSION = 2000;
|
|
957
|
-
/**
|
|
958
|
-
* Image contract of the canonical Anthropic Messages API, used when resolved
|
|
959
|
-
* model policy names no other: 8000px per side ("At least one of the image
|
|
960
|
-
* dimensions exceed max allowed size: 8000 pixels") and a 10 MB base64
|
|
961
|
-
* payload per block ("image exceeds 10 MB maximum: 14012300 bytes >
|
|
962
|
-
* 10485760 bytes"), measured on the encoded string rather than the decoded
|
|
963
|
-
* bytes. Either rejection poisons every retry and model fallback because the
|
|
964
|
-
* block stays in context, so both are clamped before the request goes out.
|
|
965
|
-
* A host with a different image contract sets `max-image-dimension` /
|
|
966
|
-
* `max-image-payload-bytes` in the KDL compatibility rules.
|
|
967
|
-
*/
|
|
968
|
-
const ANTHROPIC_MAX_IMAGE_DIMENSION = 8000;
|
|
969
|
-
const ANTHROPIC_MAX_IMAGE_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
970
|
-
/** Headroom for the surrounding JSON and any provider-side accounting. */
|
|
971
|
-
const ANTHROPIC_IMAGE_PAYLOAD_HEADROOM = 0.9;
|
|
972
|
-
/** Re-encode passes allowed to bring one image under the byte budget. */
|
|
973
|
-
const ANTHROPIC_IMAGE_RESIZE_ATTEMPTS = 4;
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Image bounds for one request. `maxDimension` and `payloadBudget` come from
|
|
977
|
-
* resolved model policy (`max-image-dimension`, `max-image-payload-bytes`) so
|
|
978
|
-
* a deployment whose image contract differs from the canonical API can
|
|
979
|
-
* override them, and the many-image path tightens `maxDimension` further
|
|
980
|
-
* because many images share one request budget.
|
|
981
|
-
*/
|
|
982
|
-
interface AnthropicImageCaps {
|
|
983
|
-
maxDimension: number;
|
|
984
|
-
payloadBudget: number;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
function countAnthropicImageBlocks(messages: Message[]): number {
|
|
988
|
-
let count = 0;
|
|
989
|
-
for (const message of messages) {
|
|
990
|
-
if (message.role !== "user" && message.role !== "developer" && message.role !== "toolResult") continue;
|
|
991
|
-
if (!Array.isArray(message.content)) continue;
|
|
992
|
-
for (const block of message.content) {
|
|
993
|
-
if (block.type === "image") count++;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return count;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Ceiling on encoder output pixels in flight. One 8000x8000 RGBA surface is
|
|
1001
|
-
* roughly 256 MiB, so admitting four of them would spike the CLI by a
|
|
1002
|
-
* gigabyte; at the full-resolution cap this yields a single pipeline, while
|
|
1003
|
-
* the many-image cap encodes at most 2000px and keeps the four-way fan-out.
|
|
1004
|
-
*/
|
|
1005
|
-
const ANTHROPIC_IMAGE_RESIZE_PIXEL_BUDGET = 16_000_000;
|
|
1006
|
-
const ANTHROPIC_IMAGE_RESIZE_MAX_CONCURRENCY = 4;
|
|
1007
|
-
|
|
1008
|
-
function anthropicImageResizeConcurrency(maxDimension: number): number {
|
|
1009
|
-
const perPipeline = maxDimension * maxDimension;
|
|
1010
|
-
if (perPipeline <= 0) return ANTHROPIC_IMAGE_RESIZE_MAX_CONCURRENCY;
|
|
1011
|
-
return Math.max(
|
|
1012
|
-
1,
|
|
1013
|
-
Math.min(ANTHROPIC_IMAGE_RESIZE_MAX_CONCURRENCY, Math.floor(ANTHROPIC_IMAGE_RESIZE_PIXEL_BUDGET / perPipeline)),
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* Memoized resize results keyed on ImageContent identity, per cap pair.
|
|
1019
|
-
* Callers keep message objects stable across turns, so without this every
|
|
1020
|
-
* request (and every in-provider retry of a fresh turn) re-decodes and
|
|
1021
|
-
* re-encodes the same oversized screenshots. A cached value identical to the
|
|
1022
|
-
* key means "already within bounds / unresizable — skip the decode". The caps
|
|
1023
|
-
* depend on image count and on resolved policy, so each pair keeps its own
|
|
1024
|
-
* cache: a value cached at 8000px is not reusable at 2000px.
|
|
1025
|
-
*/
|
|
1026
|
-
const anthropicImageResizeCaches = new Map<string, WeakMap<ImageContent, ImageContent>>();
|
|
1027
|
-
|
|
1028
|
-
function anthropicImageResizeCache(caps: AnthropicImageCaps): WeakMap<ImageContent, ImageContent> {
|
|
1029
|
-
const key = `${caps.maxDimension}:${caps.payloadBudget}`;
|
|
1030
|
-
const existing = anthropicImageResizeCaches.get(key);
|
|
1031
|
-
if (existing) return existing;
|
|
1032
|
-
const created = new WeakMap<ImageContent, ImageContent>();
|
|
1033
|
-
anthropicImageResizeCaches.set(key, created);
|
|
1034
|
-
return created;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
type ResizeLimiter = <R>(fn: () => Promise<R>) => Promise<R>;
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* Bounded-concurrency gate for image decode/encode work. The many-image path
|
|
1041
|
-
* fans out over every block of every message; unbounded, 100+ oversized images
|
|
1042
|
-
* would decode concurrently and spike memory by gigabytes. Slots are handed
|
|
1043
|
-
* off directly to the next waiter on release.
|
|
1044
|
-
*/
|
|
1045
|
-
function createResizeLimiter(limit: number): ResizeLimiter {
|
|
1046
|
-
let active = 0;
|
|
1047
|
-
const queue: (() => void)[] = [];
|
|
1048
|
-
return async fn => {
|
|
1049
|
-
if (active >= limit) {
|
|
1050
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
1051
|
-
queue.push(resolve);
|
|
1052
|
-
await promise;
|
|
1053
|
-
} else {
|
|
1054
|
-
active++;
|
|
1055
|
-
}
|
|
1056
|
-
try {
|
|
1057
|
-
return await fn();
|
|
1058
|
-
} finally {
|
|
1059
|
-
const next = queue.shift();
|
|
1060
|
-
if (next) next();
|
|
1061
|
-
else active--;
|
|
1062
|
-
}
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
async function resizeAnthropicImageBlock(block: ImageContent, caps: AnthropicImageCaps): Promise<ImageContent> {
|
|
1067
|
-
const { maxDimension, payloadBudget } = caps;
|
|
1068
|
-
try {
|
|
1069
|
-
const inputBuffer = Buffer.from(block.data, "base64");
|
|
1070
|
-
const { width, height } = await new Bun.Image(inputBuffer).metadata();
|
|
1071
|
-
if (!width || !height) return block;
|
|
1072
|
-
const overDimension = width > maxDimension || height > maxDimension;
|
|
1073
|
-
const overBudget = block.data.length > payloadBudget;
|
|
1074
|
-
if (!overDimension && !overBudget) return block;
|
|
1075
|
-
|
|
1076
|
-
// Fitting the pixel cap does not imply fitting the byte cap: a dense
|
|
1077
|
-
// screenshot re-encodes large even at the cap. Shrink by the overshoot in
|
|
1078
|
-
// area terms until the payload fits or the passes run out.
|
|
1079
|
-
let scale = Math.min(1, maxDimension / width, maxDimension / height);
|
|
1080
|
-
let best: { buffer: Uint8Array; mimeType: string } | undefined;
|
|
1081
|
-
for (let attempt = 0; attempt < ANTHROPIC_IMAGE_RESIZE_ATTEMPTS; attempt++) {
|
|
1082
|
-
const targetWidth = Math.max(1, Math.min(maxDimension, Math.round(width * scale)));
|
|
1083
|
-
const targetHeight = Math.max(1, Math.min(maxDimension, Math.round(height * scale)));
|
|
1084
|
-
// One encoder at a time: a full-resolution surface is hundreds of
|
|
1085
|
-
// megabytes, so the two candidates never coexist.
|
|
1086
|
-
const png = await new Bun.Image(inputBuffer).resize(targetWidth, targetHeight).png().bytes();
|
|
1087
|
-
const jpeg = await new Bun.Image(inputBuffer).resize(targetWidth, targetHeight).jpeg({ quality: 85 }).bytes();
|
|
1088
|
-
best =
|
|
1089
|
-
png.length <= jpeg.length
|
|
1090
|
-
? { buffer: png, mimeType: "image/png" }
|
|
1091
|
-
: { buffer: jpeg, mimeType: "image/jpeg" };
|
|
1092
|
-
|
|
1093
|
-
// Base64 expands three bytes into four characters; the encoded length
|
|
1094
|
-
// is what Anthropic measures, so derive it without encoding.
|
|
1095
|
-
const encoded = Math.ceil(best.buffer.length / 3) * 4;
|
|
1096
|
-
if (encoded <= payloadBudget) break;
|
|
1097
|
-
if (targetWidth === 1 && targetHeight === 1) break;
|
|
1098
|
-
scale *= Math.max(0.25, Math.sqrt(payloadBudget / encoded) * 0.9);
|
|
1099
|
-
}
|
|
1100
|
-
if (!best) return block;
|
|
1101
|
-
|
|
1102
|
-
const data = Buffer.from(best.buffer).toString("base64");
|
|
1103
|
-
// An image inside the pixel cap is only worth replacing if it shrank.
|
|
1104
|
-
if (!overDimension && data.length >= block.data.length) return block;
|
|
1105
|
-
// The rebuilt block carries no `url` or `providerFile`: those reference
|
|
1106
|
-
// the original bytes, and Anthropic validates a referenced image after
|
|
1107
|
-
// fetching it, so keeping either would resend the oversized copy.
|
|
1108
|
-
return {
|
|
1109
|
-
type: "image",
|
|
1110
|
-
data,
|
|
1111
|
-
mimeType: best.mimeType,
|
|
1112
|
-
};
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
logger.warn("anthropic: failed to resize oversized image", {
|
|
1115
|
-
mimeType: block.mimeType,
|
|
1116
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1117
|
-
});
|
|
1118
|
-
return block;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
async function resizeAnthropicImageContent(
|
|
1123
|
-
content: (TextContent | ImageContent)[],
|
|
1124
|
-
state: { resized: number },
|
|
1125
|
-
limit: ResizeLimiter,
|
|
1126
|
-
caps: AnthropicImageCaps,
|
|
1127
|
-
): Promise<(TextContent | ImageContent)[]> {
|
|
1128
|
-
let changed = false;
|
|
1129
|
-
const cache = anthropicImageResizeCache(caps);
|
|
1130
|
-
const next = await Promise.all(
|
|
1131
|
-
content.map(async block => {
|
|
1132
|
-
if (block.type !== "image") return block;
|
|
1133
|
-
let resized = cache.get(block);
|
|
1134
|
-
if (resized === undefined) {
|
|
1135
|
-
resized = await limit(() => resizeAnthropicImageBlock(block, caps));
|
|
1136
|
-
cache.set(block, resized);
|
|
1137
|
-
}
|
|
1138
|
-
if (resized !== block) {
|
|
1139
|
-
changed = true;
|
|
1140
|
-
state.resized++;
|
|
1141
|
-
}
|
|
1142
|
-
return resized;
|
|
1143
|
-
}),
|
|
1144
|
-
);
|
|
1145
|
-
return changed ? next : content;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
async function resizeAnthropicImageMessage(
|
|
1149
|
-
message: Message,
|
|
1150
|
-
state: { resized: number },
|
|
1151
|
-
limit: ResizeLimiter,
|
|
1152
|
-
caps: AnthropicImageCaps,
|
|
1153
|
-
): Promise<Message> {
|
|
1154
|
-
if (message.role === "user" || message.role === "developer") {
|
|
1155
|
-
if (!Array.isArray(message.content)) return message;
|
|
1156
|
-
const content = await resizeAnthropicImageContent(message.content, state, limit, caps);
|
|
1157
|
-
return content === message.content ? message : { ...message, content };
|
|
1158
|
-
}
|
|
1159
|
-
if (message.role === "toolResult") {
|
|
1160
|
-
const content = await resizeAnthropicImageContent(message.content, state, limit, caps);
|
|
1161
|
-
return content === message.content ? message : { ...message, content };
|
|
1162
|
-
}
|
|
1163
|
-
return message;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
async function prepareAnthropicImageContext(context: Context, model: Model<"anthropic-messages">): Promise<Context> {
|
|
1167
|
-
if (!model.input.includes("image")) return context;
|
|
1168
|
-
const imageCount = countAnthropicImageBlocks(context.messages);
|
|
1169
|
-
if (imageCount === 0) return context;
|
|
1170
|
-
// Many images share one request size budget, so they get the tighter cap.
|
|
1171
|
-
// A smaller set only has to clear the host's per-image limit.
|
|
1172
|
-
const hostMaxDimension = model.compat.maxImageDimension ?? ANTHROPIC_MAX_IMAGE_DIMENSION;
|
|
1173
|
-
const maxDimension =
|
|
1174
|
-
imageCount > ANTHROPIC_MANY_IMAGE_THRESHOLD
|
|
1175
|
-
? Math.min(ANTHROPIC_MANY_IMAGE_MAX_DIMENSION, hostMaxDimension)
|
|
1176
|
-
: hostMaxDimension;
|
|
1177
|
-
const maxPayloadBytes = model.compat.maxImagePayloadBytes ?? ANTHROPIC_MAX_IMAGE_PAYLOAD_BYTES;
|
|
1178
|
-
const caps: AnthropicImageCaps = {
|
|
1179
|
-
maxDimension,
|
|
1180
|
-
payloadBudget: Math.floor(maxPayloadBytes * ANTHROPIC_IMAGE_PAYLOAD_HEADROOM),
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
let changed = false;
|
|
1184
|
-
const state = { resized: 0 };
|
|
1185
|
-
const limit = createResizeLimiter(anthropicImageResizeConcurrency(maxDimension));
|
|
1186
|
-
const messages = await Promise.all(
|
|
1187
|
-
context.messages.map(async message => {
|
|
1188
|
-
const next = await resizeAnthropicImageMessage(message, state, limit, caps);
|
|
1189
|
-
if (next !== message) changed = true;
|
|
1190
|
-
return next;
|
|
1191
|
-
}),
|
|
1192
|
-
);
|
|
1193
|
-
if (!changed) return context;
|
|
1194
|
-
logger.debug("anthropic: resized oversized images", {
|
|
1195
|
-
imageCount,
|
|
1196
|
-
resized: state.resized,
|
|
1197
|
-
maxDimension,
|
|
1198
|
-
});
|
|
1199
|
-
return { ...context, messages };
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
984
|
type AnthropicImageSource =
|
|
1203
985
|
| { type: "base64"; media_type: AnthropicImageMediaType; data: string }
|
|
1204
986
|
| { type: "url"; url: string }
|
|
@@ -1340,7 +1122,8 @@ export interface AnthropicOptions extends StreamOptions {
|
|
|
1340
1122
|
* header is auto-attached; the response parser then honors mid-stream
|
|
1341
1123
|
* `fallback` content blocks and `usage.iterations` for served-model
|
|
1342
1124
|
* promotion and per-attempt pricing. Opt-in ONLY — leaving this
|
|
1343
|
-
* undefined
|
|
1125
|
+
* undefined sends neither and keeps both off; a `fallback` block in the
|
|
1126
|
+
* response (a gateway opted in) is still persisted as a replay boundary.
|
|
1344
1127
|
*/
|
|
1345
1128
|
fallbacks?: FallbackParam[];
|
|
1346
1129
|
}
|
|
@@ -1355,6 +1138,8 @@ export type AnthropicClientOptionsArgs = {
|
|
|
1355
1138
|
dynamicHeaders?: Record<string, string>;
|
|
1356
1139
|
isOAuth?: boolean;
|
|
1357
1140
|
hasTools?: boolean;
|
|
1141
|
+
/** Whether a tool in ANTHROPIC_BUFFERED_INPUT_TOOLS is present; suppresses the request-wide fine-grained streaming beta. */
|
|
1142
|
+
hasBufferedInputTools?: boolean;
|
|
1358
1143
|
thinkingEnabled?: boolean;
|
|
1359
1144
|
thinkingDisplay?: AnthropicThinkingDisplay;
|
|
1360
1145
|
disableStrictTools?: boolean;
|
|
@@ -1428,7 +1213,7 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
|
|
|
1428
1213
|
if (model.provider === "github-copilot") {
|
|
1429
1214
|
return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
|
|
1430
1215
|
}
|
|
1431
|
-
if (model.
|
|
1216
|
+
if (model.compat.firstPartyProvider) return resolveDirectAnthropicBaseUrl(model);
|
|
1432
1217
|
return normalizeAnthropicBaseUrl(model.baseUrl);
|
|
1433
1218
|
}
|
|
1434
1219
|
|
|
@@ -1491,7 +1276,7 @@ function resolveAnthropicCustomHeaders(
|
|
|
1491
1276
|
model: Model<"anthropic-messages">,
|
|
1492
1277
|
baseUrl: string | undefined,
|
|
1493
1278
|
): Record<string, string> | undefined {
|
|
1494
|
-
if (model.
|
|
1279
|
+
if (!model.compat.firstPartyProvider) return undefined;
|
|
1495
1280
|
return resolveAnthropicCustomHeadersForBaseUrl(baseUrl);
|
|
1496
1281
|
}
|
|
1497
1282
|
|
|
@@ -1523,7 +1308,7 @@ function resolvePemValue(value: string | undefined, name: string): string | unde
|
|
|
1523
1308
|
}
|
|
1524
1309
|
|
|
1525
1310
|
function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTlsOptions | undefined {
|
|
1526
|
-
if (model.
|
|
1311
|
+
if (!model.compat.firstPartyProvider) return undefined;
|
|
1527
1312
|
if (!isFoundryEnabled()) return undefined;
|
|
1528
1313
|
|
|
1529
1314
|
const cacheKey = foundryTlsOptionsCacheKey();
|
|
@@ -1552,7 +1337,7 @@ function buildClaudeCodeTlsFetchOptions(
|
|
|
1552
1337
|
model: Model<"anthropic-messages">,
|
|
1553
1338
|
baseUrl: string | undefined,
|
|
1554
1339
|
): AnthropicFetchOptions | undefined {
|
|
1555
|
-
if (model.
|
|
1340
|
+
if (!model.compat.firstPartyProvider) return undefined;
|
|
1556
1341
|
if (!baseUrl) return undefined;
|
|
1557
1342
|
|
|
1558
1343
|
let serverName: string;
|
|
@@ -1797,6 +1582,10 @@ function unwrapAnthropicThinkingEnvelope(text: string): string | undefined {
|
|
|
1797
1582
|
* The refused response's content as the continuation prefix: client tool
|
|
1798
1583
|
* calls (no matching tool_result) are omitted and a trailing text block is
|
|
1799
1584
|
* right-trimmed, per the fallback-credit continuation contract.
|
|
1585
|
+
*
|
|
1586
|
+
* A `fallback` boundary stays exactly where it appeared: the API accepts it on
|
|
1587
|
+
* any request, beta or not, and uses its position to validate the thinking
|
|
1588
|
+
* blocks on either side, so the echo must not collapse or move it.
|
|
1800
1589
|
*/
|
|
1801
1590
|
function refusalContinuationPrefix(content: AssistantMessage["content"]): AssistantMessage["content"] {
|
|
1802
1591
|
const prefix = structuredClone(content.filter(block => block.type !== "toolCall"));
|
|
@@ -1805,40 +1594,54 @@ function refusalContinuationPrefix(content: AssistantMessage["content"]): Assist
|
|
|
1805
1594
|
return prefix;
|
|
1806
1595
|
}
|
|
1807
1596
|
|
|
1808
|
-
/**
|
|
1809
|
-
|
|
1810
|
-
|
|
1597
|
+
/**
|
|
1598
|
+
* Wire form of {@link refusalContinuationPrefix} for the appended assistant
|
|
1599
|
+
* message. Blocks with no valid continuation wire shape (unsigned thinking,
|
|
1600
|
+
* anything the Anthropic stream cannot have produced) are dropped.
|
|
1601
|
+
*/
|
|
1602
|
+
function formatEchoedRefusalContent(content: AssistantMessage["content"]): ContentBlockParam[] {
|
|
1603
|
+
const echoed: ContentBlockParam[] = [];
|
|
1604
|
+
for (const block of refusalContinuationPrefix(content)) {
|
|
1811
1605
|
switch (block.type) {
|
|
1812
1606
|
case "text":
|
|
1813
|
-
|
|
1607
|
+
echoed.push({ type: "text", text: block.text });
|
|
1608
|
+
break;
|
|
1814
1609
|
case "thinking":
|
|
1815
|
-
|
|
1816
|
-
type: "thinking",
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
};
|
|
1610
|
+
if (block.thinkingSignature) {
|
|
1611
|
+
echoed.push({ type: "thinking", thinking: block.thinking, signature: block.thinkingSignature });
|
|
1612
|
+
}
|
|
1613
|
+
break;
|
|
1820
1614
|
case "redactedThinking":
|
|
1821
|
-
|
|
1615
|
+
echoed.push({ type: "redacted_thinking", data: block.data });
|
|
1616
|
+
break;
|
|
1822
1617
|
case "fallback":
|
|
1823
|
-
|
|
1618
|
+
echoed.push({ type: "fallback", from: block.from, to: block.to });
|
|
1619
|
+
break;
|
|
1824
1620
|
case "anthropicServerTool":
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
return block;
|
|
1621
|
+
echoed.push(block.block);
|
|
1622
|
+
break;
|
|
1828
1623
|
}
|
|
1829
|
-
}
|
|
1624
|
+
}
|
|
1625
|
+
return echoed;
|
|
1830
1626
|
}
|
|
1831
1627
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1628
|
+
/**
|
|
1629
|
+
* The refused request's frozen body retargeted at the fallback model with the
|
|
1630
|
+
* credit token, optionally followed by the echoed refusal prefix.
|
|
1631
|
+
*/
|
|
1632
|
+
function fallbackCreditRequestBody(
|
|
1633
|
+
redemption: AnthropicFallbackCreditHandle,
|
|
1634
|
+
targetModelId: string,
|
|
1635
|
+
continuation: ContentBlockParam[] | undefined,
|
|
1636
|
+
): MessageCreateParamsStreaming {
|
|
1637
|
+
const body = structuredClone(redemption.params);
|
|
1638
|
+
body.model = targetModelId;
|
|
1639
|
+
body.fallback_credit_token = redemption.token;
|
|
1640
|
+
delete body.fallbacks;
|
|
1641
|
+
if (continuation && continuation.length > 0) {
|
|
1642
|
+
body.messages = [...body.messages, { role: "assistant", content: continuation }];
|
|
1840
1643
|
}
|
|
1841
|
-
return
|
|
1644
|
+
return body;
|
|
1842
1645
|
}
|
|
1843
1646
|
|
|
1844
1647
|
function createEmptyUsage(premiumRequests?: number): Usage {
|
|
@@ -2197,6 +2000,33 @@ function rememberPrefixDroppedThinking(
|
|
|
2197
2000
|
}
|
|
2198
2001
|
}
|
|
2199
2002
|
|
|
2003
|
+
/**
|
|
2004
|
+
* Replay keys of the thinking blocks in the request's latest assistant turn.
|
|
2005
|
+
* Anthropic validates every assistant message after the last user message
|
|
2006
|
+
* carrying more than tool results as one "latest assistant message" (its error
|
|
2007
|
+
* indices count blocks across that whole run).
|
|
2008
|
+
*/
|
|
2009
|
+
function latestAssistantTurnThinkingKeys(params: MessageCreateParamsStreaming): string[] {
|
|
2010
|
+
const keys: string[] = [];
|
|
2011
|
+
for (let index = params.messages.length - 1; index >= 0; index--) {
|
|
2012
|
+
const message = params.messages[index]!;
|
|
2013
|
+
if (message.role !== "assistant") {
|
|
2014
|
+
const toolResultsOnly =
|
|
2015
|
+
Array.isArray(message.content) &&
|
|
2016
|
+
message.content.length > 0 &&
|
|
2017
|
+
message.content.every(block => block.type === "tool_result");
|
|
2018
|
+
if (!toolResultsOnly) break;
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
if (!Array.isArray(message.content)) continue;
|
|
2022
|
+
for (const block of message.content) {
|
|
2023
|
+
const key = thinkingReplayKey(block);
|
|
2024
|
+
if (key) keys.push(key);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return keys;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2200
2030
|
function rememberPrefixBindingFailure(
|
|
2201
2031
|
params: MessageCreateParamsStreaming,
|
|
2202
2032
|
message: string,
|
|
@@ -2406,13 +2236,33 @@ const streamAnthropicOnce = (
|
|
|
2406
2236
|
};
|
|
2407
2237
|
let dropAllThinking = droppedAllThinkingForSignature;
|
|
2408
2238
|
let prefixBindingRetryAttempted = false;
|
|
2239
|
+
let latestTurnThinkingRetryAttempted = false;
|
|
2240
|
+
// Claude Code's `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`; the wrapper
|
|
2241
|
+
// already turned the gated compat off, this covers caller/frozen inputs.
|
|
2242
|
+
const killSwitchActive = experimentalBetasDisabled(model);
|
|
2409
2243
|
// A live fallback-credit redemption replays its frozen body.
|
|
2410
2244
|
const frozenRedemption =
|
|
2411
|
-
options?.fallbackCreditRedemption &&
|
|
2412
|
-
Date.now() <= options.fallbackCreditRedemption.expiresAt &&
|
|
2413
|
-
options.fallbackCreditRedemption.params
|
|
2245
|
+
options?.fallbackCreditRedemption && Date.now() <= options.fallbackCreditRedemption.expiresAt
|
|
2414
2246
|
? options.fallbackCreditRedemption
|
|
2415
2247
|
: undefined;
|
|
2248
|
+
// A body frozen before the kill switch was set still carries the fields
|
|
2249
|
+
// it withholds; the credit token is bound to that body, so forfeit it
|
|
2250
|
+
// and build a fresh request. Server tools that already ran would re-run
|
|
2251
|
+
// and re-bill on a tokenless retry, so surface that case instead.
|
|
2252
|
+
const frozenBody = frozenRedemption?.params;
|
|
2253
|
+
// The redemption drops `fallbacks` before sending, so they cannot force a forfeit.
|
|
2254
|
+
const forfeitFrozenForKillSwitch =
|
|
2255
|
+
killSwitchActive &&
|
|
2256
|
+
frozenBody !== undefined &&
|
|
2257
|
+
carriesKillSwitchField({ ...frozenBody, fallbacks: undefined });
|
|
2258
|
+
if (
|
|
2259
|
+
forfeitFrozenForKillSwitch &&
|
|
2260
|
+
frozenRedemption?.refusedContent?.some(block => block.type === "anthropicServerTool")
|
|
2261
|
+
) {
|
|
2262
|
+
throw new AIError.ConfigurationError(
|
|
2263
|
+
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS was set after a refused turn that ran server tools; its fallback credit cannot be redeemed without the experimental fields the switch withholds.",
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2416
2266
|
// Collected at most once, on first params build; a frozen redemption
|
|
2417
2267
|
// body never builds params.
|
|
2418
2268
|
let controlRecords: AnthropicControlRecord[] | undefined;
|
|
@@ -2431,12 +2281,16 @@ const streamAnthropicOnce = (
|
|
|
2431
2281
|
// `anthropic-beta` value, so once breakpoints are dropped an unstripped
|
|
2432
2282
|
// base would re-attach the very cache beta the replay abandoned. Both
|
|
2433
2283
|
// prompt-cache betas govern `ttl`/`scope`, so the options rung has to
|
|
2434
|
-
// strip them too.
|
|
2435
|
-
//
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2284
|
+
// strip them too. The kill switch's betas never ride, whatever the
|
|
2285
|
+
// caller's header says. Read through a closure because both cache
|
|
2286
|
+
// flags flip mid-turn.
|
|
2287
|
+
const callerBetaBaseHeaders = (): Record<string, string> => {
|
|
2288
|
+
const headers =
|
|
2289
|
+
dropCacheControl || dropCacheControlOptions
|
|
2290
|
+
? stripHeaderBetas(mergedCallerHeaders, promptCacheBetas)
|
|
2291
|
+
: mergedCallerHeaders;
|
|
2292
|
+
return killSwitchActive ? stripHeaderBetas(headers, killSwitchBetas) : headers;
|
|
2293
|
+
};
|
|
2440
2294
|
// Keep fallback payloads aligned with the top-level Vertex effort gate:
|
|
2441
2295
|
// no nested effort field means the fallback scan cannot re-add its beta.
|
|
2442
2296
|
let fallbacks = options?.fallbacks;
|
|
@@ -2551,12 +2405,14 @@ const streamAnthropicOnce = (
|
|
|
2551
2405
|
// `dropCacheControlOptions` short-circuits it because the breakpoint
|
|
2552
2406
|
// it does carry has no `ttl` left to authorize. Both this gate and
|
|
2553
2407
|
// buildParams resolve the breakpoint behind the same flags so the
|
|
2554
|
-
// header and the body can never disagree.
|
|
2408
|
+
// header and the body can never disagree. Vertex rawPredict honors
|
|
2409
|
+
// `ttl: "1h"` without the beta and rejects it in either channel.
|
|
2555
2410
|
const isOAuth = options?.isOAuth ?? isAnthropicOAuthToken(apiKey);
|
|
2556
2411
|
if (
|
|
2557
2412
|
!isOAuth &&
|
|
2558
2413
|
!dropCacheControl &&
|
|
2559
2414
|
!dropCacheControlOptions &&
|
|
2415
|
+
!isVertexRawPredictUrl(baseUrl) &&
|
|
2560
2416
|
getCacheControl(model, options?.cacheRetention, isOAuth).cacheControl?.ttl === "1h" &&
|
|
2561
2417
|
!extraBetas.includes(extendedCacheTtlBeta)
|
|
2562
2418
|
) {
|
|
@@ -2565,7 +2421,9 @@ const streamAnthropicOnce = (
|
|
|
2565
2421
|
if (!isOAuth && isOfficialAnthropicApiUrl(baseUrl) && !extraBetas.includes(fallbackCreditBeta)) {
|
|
2566
2422
|
extraBetas.push(fallbackCreditBeta);
|
|
2567
2423
|
}
|
|
2568
|
-
|
|
2424
|
+
// A forfeited frozen body is rebuilt fresh, so it keeps the betas
|
|
2425
|
+
// computed above instead of the saved list describing the old body.
|
|
2426
|
+
if (options?.fallbackCreditRedemption && !forfeitFrozenForKillSwitch) {
|
|
2569
2427
|
const frozenBetas = (options.fallbackCreditRedemption.betas ?? []).filter(
|
|
2570
2428
|
b => !b.startsWith("server-side-fallback-"),
|
|
2571
2429
|
);
|
|
@@ -2573,7 +2431,7 @@ const streamAnthropicOnce = (
|
|
|
2573
2431
|
for (const beta of frozenBetas) {
|
|
2574
2432
|
extraBetas.push(beta);
|
|
2575
2433
|
}
|
|
2576
|
-
if (!extraBetas.
|
|
2434
|
+
if (!extraBetas.some(beta => beta.startsWith("fallback-credit-"))) {
|
|
2577
2435
|
extraBetas.push(fallbackCreditBeta);
|
|
2578
2436
|
}
|
|
2579
2437
|
}
|
|
@@ -2598,6 +2456,8 @@ const streamAnthropicOnce = (
|
|
|
2598
2456
|
}
|
|
2599
2457
|
}
|
|
2600
2458
|
}
|
|
2459
|
+
// Caller betas and a frozen redemption's beta list bypass compat.
|
|
2460
|
+
if (killSwitchActive) extraBetas = extraBetas.filter(beta => killSwitchBetas[beta] !== true);
|
|
2601
2461
|
|
|
2602
2462
|
clientArgs = {
|
|
2603
2463
|
model,
|
|
@@ -2609,6 +2469,8 @@ const streamAnthropicOnce = (
|
|
|
2609
2469
|
dynamicHeaders: copilotDynamicHeaders?.headers,
|
|
2610
2470
|
isOAuth: options?.isOAuth,
|
|
2611
2471
|
hasTools: !!context.tools?.length,
|
|
2472
|
+
hasBufferedInputTools:
|
|
2473
|
+
context.tools?.some(tool => ANTHROPIC_BUFFERED_INPUT_TOOLS.has(tool.name)) ?? false,
|
|
2612
2474
|
thinkingEnabled: options?.thinkingEnabled,
|
|
2613
2475
|
thinkingDisplay: options?.thinkingDisplay,
|
|
2614
2476
|
fetch: options?.fetch,
|
|
@@ -2659,6 +2521,8 @@ const streamAnthropicOnce = (
|
|
|
2659
2521
|
if (replacementPayload !== undefined) {
|
|
2660
2522
|
nextParams = replacementPayload as typeof nextParams;
|
|
2661
2523
|
}
|
|
2524
|
+
// After `onPayload`, so a hook cannot restore a field the switch withholds.
|
|
2525
|
+
if (killSwitchActive) withholdKillSwitchFields(nextParams);
|
|
2662
2526
|
if (nextParams.compaction) stripCompactionIncompatibleParams(nextParams);
|
|
2663
2527
|
// After `onPayload`, so a hook cannot restore a field Bedrock rejects.
|
|
2664
2528
|
if (model.compat.bedrockMessagesApi) fitBedrockAnthropicPayload(nextParams);
|
|
@@ -2696,31 +2560,18 @@ const streamAnthropicOnce = (
|
|
|
2696
2560
|
let params: MessageCreateParamsStreaming;
|
|
2697
2561
|
// Recheck expiry here: preparation (image resizing) is async, and an
|
|
2698
2562
|
// expired token must fall back to a fresh request as before.
|
|
2699
|
-
if (frozenRedemption && Date.now() <= frozenRedemption.expiresAt) {
|
|
2700
|
-
const redemption = frozenRedemption;
|
|
2563
|
+
if (frozenRedemption && !forfeitFrozenForKillSwitch && Date.now() <= frozenRedemption.expiresAt) {
|
|
2701
2564
|
usingFallbackCredit = true;
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
)
|
|
2712
|
-
fallbackCreditShape = "continuation";
|
|
2713
|
-
const echoed = formatEchoedRefusalContent(redemption.refusedContent);
|
|
2714
|
-
if (echoed.length > 0) {
|
|
2715
|
-
frozenParams.messages = [
|
|
2716
|
-
...frozenParams.messages,
|
|
2717
|
-
{ role: "assistant", content: echoed } as unknown as (typeof frozenParams.messages)[number],
|
|
2718
|
-
];
|
|
2719
|
-
}
|
|
2720
|
-
} else {
|
|
2721
|
-
fallbackCreditShape = "unchanged";
|
|
2722
|
-
}
|
|
2723
|
-
params = frozenParams;
|
|
2565
|
+
const echoed =
|
|
2566
|
+
frozenRedemption.prefillClaim !== false && frozenRedemption.refusedContent
|
|
2567
|
+
? formatEchoedRefusalContent(frozenRedemption.refusedContent)
|
|
2568
|
+
: [];
|
|
2569
|
+
fallbackCreditShape = echoed.length > 0 ? "continuation" : "unchanged";
|
|
2570
|
+
params = fallbackCreditRequestBody(
|
|
2571
|
+
frozenRedemption,
|
|
2572
|
+
options?.requestModelId ?? model.requestModelId ?? model.id,
|
|
2573
|
+
echoed,
|
|
2574
|
+
);
|
|
2724
2575
|
rawRequestDump = {
|
|
2725
2576
|
provider: model.provider,
|
|
2726
2577
|
api: output.api,
|
|
@@ -2760,11 +2611,14 @@ const streamAnthropicOnce = (
|
|
|
2760
2611
|
if (unwrappedThinking !== undefined) {
|
|
2761
2612
|
block.thinking = unwrappedThinking;
|
|
2762
2613
|
block.thinkingSignature = undefined;
|
|
2763
|
-
} else if (
|
|
2614
|
+
} else if (block.thinkingSignature) {
|
|
2764
2615
|
// The signature names the model that actually produced the
|
|
2765
2616
|
// block; a gateway serving a different model than requested
|
|
2766
2617
|
// cannot mint one that says otherwise.
|
|
2767
|
-
output.upstreamModel
|
|
2618
|
+
if (!output.upstreamModel) {
|
|
2619
|
+
output.upstreamModel = servedModelFromAnthropicSignature(block.thinkingSignature);
|
|
2620
|
+
}
|
|
2621
|
+
if (isAnthropicNarrationSignature(block.thinkingSignature)) block.narration = true;
|
|
2768
2622
|
}
|
|
2769
2623
|
stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
|
|
2770
2624
|
} else if (block.type === "anthropicServerTool" && block.block.type === "server_tool_use") {
|
|
@@ -2849,7 +2703,7 @@ const streamAnthropicOnce = (
|
|
|
2849
2703
|
// max-wait budget covers the whole wait, not one attempt.
|
|
2850
2704
|
const slowMode =
|
|
2851
2705
|
options?.anthropicSlowMode !== undefined &&
|
|
2852
|
-
model.
|
|
2706
|
+
model.compat.firstPartyProvider &&
|
|
2853
2707
|
isOAuthToken &&
|
|
2854
2708
|
!options.client &&
|
|
2855
2709
|
isOfficialAnthropicApiUrl(baseUrl)
|
|
@@ -2925,8 +2779,13 @@ const streamAnthropicOnce = (
|
|
|
2925
2779
|
const rawBetas = options.fallbackCreditRedemption.betaHeader
|
|
2926
2780
|
.split(",")
|
|
2927
2781
|
.map(b => b.trim())
|
|
2928
|
-
.filter(
|
|
2929
|
-
|
|
2782
|
+
.filter(
|
|
2783
|
+
b =>
|
|
2784
|
+
b &&
|
|
2785
|
+
!b.startsWith("server-side-fallback-") &&
|
|
2786
|
+
(!killSwitchActive || killSwitchBetas[b] !== true),
|
|
2787
|
+
);
|
|
2788
|
+
if (!rawBetas.some(beta => beta.startsWith("fallback-credit-"))) {
|
|
2930
2789
|
rawBetas.push(fallbackCreditBeta);
|
|
2931
2790
|
}
|
|
2932
2791
|
perRequestHeaders = {
|
|
@@ -3156,15 +3015,15 @@ const streamAnthropicOnce = (
|
|
|
3156
3015
|
}
|
|
3157
3016
|
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
3158
3017
|
if (event.content_block.type === "fallback") {
|
|
3159
|
-
//
|
|
3160
|
-
//
|
|
3161
|
-
//
|
|
3162
|
-
//
|
|
3018
|
+
// The boundary between the primary's output and the
|
|
3019
|
+
// fallback model's. Persist it even when this request did
|
|
3020
|
+
// not opt into the beta chain: a gateway can opt in on the
|
|
3021
|
+
// client's behalf, and without the marker the replay
|
|
3022
|
+
// converter cannot tell the two models' signed reasoning
|
|
3023
|
+
// apart (see `collapseFallbackSegments`).
|
|
3163
3024
|
const fallback = parseAnthropicFallbackWireBlock(event.content_block);
|
|
3164
|
-
if (!
|
|
3165
|
-
|
|
3166
|
-
reportAnthropicEnvelopeAnomaly("fallback content_block missing model refs");
|
|
3167
|
-
}
|
|
3025
|
+
if (!fallback) {
|
|
3026
|
+
reportAnthropicEnvelopeAnomaly("fallback content_block missing model refs");
|
|
3168
3027
|
openBlocks.set(event.index, { contentIndex: -1, kind: "ignored" });
|
|
3169
3028
|
continue;
|
|
3170
3029
|
}
|
|
@@ -3178,8 +3037,9 @@ const streamAnthropicOnce = (
|
|
|
3178
3037
|
// classifier block on the primary was retried on the
|
|
3179
3038
|
// fallback model. Adopt the served id immediately so
|
|
3180
3039
|
// pricing decisions downstream (final usage.iterations may
|
|
3181
|
-
// arrive before/after) see the right model.
|
|
3182
|
-
|
|
3040
|
+
// arrive before/after) see the right model. Served-model
|
|
3041
|
+
// promotion and per-iteration pricing stay opt-in.
|
|
3042
|
+
if (serverSideFallback) output.model = fallback.to.model;
|
|
3183
3043
|
continue;
|
|
3184
3044
|
}
|
|
3185
3045
|
if (event.content_block.type === "text") {
|
|
@@ -3436,7 +3296,9 @@ const streamAnthropicOnce = (
|
|
|
3436
3296
|
const label = category ? `Refusal (${category})` : "Refusal";
|
|
3437
3297
|
output.errorMessage = explanation ? `${label}: ${explanation}` : label;
|
|
3438
3298
|
}
|
|
3439
|
-
|
|
3299
|
+
// Only a refusal carries a credit token. Refusal turns are never
|
|
3300
|
+
// written to the session, so the frozen body stays in memory.
|
|
3301
|
+
if (stopDetails?.type === "refusal" && stopDetails.fallback_credit_token) {
|
|
3440
3302
|
const sentBetaHeader =
|
|
3441
3303
|
getHeaderCaseInsensitive(perRequestHeaders ?? {}, "anthropic-beta") ??
|
|
3442
3304
|
getHeaderCaseInsensitive(clientDefaultHeaders ?? {}, "anthropic-beta") ??
|
|
@@ -3447,7 +3309,7 @@ const streamAnthropicOnce = (
|
|
|
3447
3309
|
params: structuredClone(params),
|
|
3448
3310
|
betas: Array.from(requestExtraBetas),
|
|
3449
3311
|
betaHeader: sentBetaHeader,
|
|
3450
|
-
expiresAt: Date.now() +
|
|
3312
|
+
expiresAt: Date.now() + FALLBACK_CREDIT_TTL_MS,
|
|
3451
3313
|
refusedContent: output.content ? structuredClone(output.content) : undefined,
|
|
3452
3314
|
};
|
|
3453
3315
|
}
|
|
@@ -3569,9 +3431,14 @@ const streamAnthropicOnce = (
|
|
|
3569
3431
|
resetStreamOutputState();
|
|
3570
3432
|
continue;
|
|
3571
3433
|
}
|
|
3572
|
-
if (
|
|
3434
|
+
if (
|
|
3435
|
+
usingFallbackCredit &&
|
|
3436
|
+
frozenRedemption &&
|
|
3437
|
+
firstTokenTime === undefined &&
|
|
3438
|
+
AIError.status(streamFailure) === 400
|
|
3439
|
+
) {
|
|
3573
3440
|
const errMessage = streamFailure instanceof Error ? streamFailure.message : String(streamFailure);
|
|
3574
|
-
const redemption =
|
|
3441
|
+
const redemption = frozenRedemption;
|
|
3575
3442
|
if (errMessage.includes("redemption temporarily unavailable")) {
|
|
3576
3443
|
if (Date.now() < redemption.expiresAt && fallbackCreditTransientRetries < 2) {
|
|
3577
3444
|
fallbackCreditTransientRetries++;
|
|
@@ -3597,12 +3464,11 @@ const streamAnthropicOnce = (
|
|
|
3597
3464
|
},
|
|
3598
3465
|
);
|
|
3599
3466
|
fallbackCreditShape = "unchanged";
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
params = frozenParams;
|
|
3467
|
+
params = fallbackCreditRequestBody(
|
|
3468
|
+
redemption,
|
|
3469
|
+
options?.requestModelId ?? model.requestModelId ?? model.id,
|
|
3470
|
+
undefined,
|
|
3471
|
+
);
|
|
3606
3472
|
resetStreamOutputState();
|
|
3607
3473
|
continue;
|
|
3608
3474
|
}
|
|
@@ -3677,6 +3543,50 @@ const streamAnthropicOnce = (
|
|
|
3677
3543
|
firstTokenTime = undefined;
|
|
3678
3544
|
continue;
|
|
3679
3545
|
}
|
|
3546
|
+
if (
|
|
3547
|
+
!latestTurnThinkingRetryAttempted &&
|
|
3548
|
+
firstTokenTime === undefined &&
|
|
3549
|
+
!streamedReplayUnsafeContent &&
|
|
3550
|
+
AIError.isImmutableLatestThinkingText(streamFailureMessage)
|
|
3551
|
+
) {
|
|
3552
|
+
// The API refused the latest assistant turn's reasoning as modified
|
|
3553
|
+
// (e.g. a mid-turn fallback persisted without its boundary marker).
|
|
3554
|
+
// Resending the turn fails the same way forever, so drop its
|
|
3555
|
+
// reasoning on the wire — stored history keeps it — and retry once.
|
|
3556
|
+
// The session remembers the dropped blocks so later requests replay
|
|
3557
|
+
// the same prefix; without session state, drop every block instead.
|
|
3558
|
+
const keys = latestAssistantTurnThinkingKeys(params);
|
|
3559
|
+
if (keys.length > 0) {
|
|
3560
|
+
logger.warn(
|
|
3561
|
+
"anthropic: latest assistant turn's thinking rejected as modified, dropping it and retrying",
|
|
3562
|
+
{
|
|
3563
|
+
provider: model.provider,
|
|
3564
|
+
model: model.id,
|
|
3565
|
+
baseUrl,
|
|
3566
|
+
blocks: keys.length,
|
|
3567
|
+
},
|
|
3568
|
+
);
|
|
3569
|
+
latestTurnThinkingRetryAttempted = true;
|
|
3570
|
+
if (providerSessionState) {
|
|
3571
|
+
for (const key of keys) providerSessionState.prefixDroppedThinkingBlocks.add(key);
|
|
3572
|
+
} else {
|
|
3573
|
+
dropAllThinking = true;
|
|
3574
|
+
}
|
|
3575
|
+
params = await rebuildParams(streamFailure);
|
|
3576
|
+
providerRetryAttempt = 0;
|
|
3577
|
+
output.content.length = 0;
|
|
3578
|
+
output.model = model.id;
|
|
3579
|
+
output.responseId = undefined;
|
|
3580
|
+
output.upstreamModel = undefined;
|
|
3581
|
+
output.errorMessage = undefined;
|
|
3582
|
+
output.inputTransformations = undefined;
|
|
3583
|
+
output.providerPayload = undefined;
|
|
3584
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
3585
|
+
output.stopReason = "stop";
|
|
3586
|
+
firstTokenTime = undefined;
|
|
3587
|
+
continue;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3680
3590
|
if (
|
|
3681
3591
|
!forceDemoteUnsignedThinking &&
|
|
3682
3592
|
firstTokenTime === undefined &&
|
|
@@ -3927,18 +3837,16 @@ const streamAnthropicOnce = (
|
|
|
3927
3837
|
firstTokenTime = undefined;
|
|
3928
3838
|
}
|
|
3929
3839
|
}
|
|
3930
|
-
if (
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
// AssistantMessage form so later replays keep signatures and server tools.
|
|
3937
|
-
output.content.unshift(...refusalContinuationPrefix(options.fallbackCreditRedemption.refusedContent));
|
|
3840
|
+
if (usingFallbackCredit && fallbackCreditShape === "continuation" && frozenRedemption?.refusedContent) {
|
|
3841
|
+
// The response continues the echoed prefix; keep it, signatures included, in
|
|
3842
|
+
// the stored turn. A tool loop requires the turn to open with its thinking
|
|
3843
|
+
// block, and a later rejection of the refused model's signature is
|
|
3844
|
+
// recovered by the invalid-signature retry.
|
|
3845
|
+
output.content.unshift(...refusalContinuationPrefix(frozenRedemption.refusedContent));
|
|
3938
3846
|
}
|
|
3939
3847
|
output.duration = performance.now() - startTime;
|
|
3940
3848
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
3941
|
-
if (dropFastMode && model.
|
|
3849
|
+
if (dropFastMode && model.compat.firstPartyProvider && options?.serviceTier === "priority") {
|
|
3942
3850
|
output.disabledFeatures = [...(output.disabledFeatures ?? []), "priority"];
|
|
3943
3851
|
}
|
|
3944
3852
|
if (forceDemoteUnsignedThinking && model.compat.replayUnsignedThinking) {
|
|
@@ -4003,10 +3911,97 @@ const streamAnthropicOnce = (
|
|
|
4003
3911
|
* loop. The inner attempt owns Anthropic provider-failure retries.
|
|
4004
3912
|
*/
|
|
4005
3913
|
export const streamAnthropic: StreamFunction<"anthropic-messages"> = (model, context, options) =>
|
|
4006
|
-
withReplaySafeStreamRetry(model, context, options, streamAnthropicOnce, {
|
|
3914
|
+
withReplaySafeStreamRetry(withoutDisabledExperimentalBetas(model), context, options, streamAnthropicOnce, {
|
|
4007
3915
|
retryEmptyCompletion: true,
|
|
4008
3916
|
});
|
|
4009
3917
|
|
|
3918
|
+
/** Whether Claude Code's `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` applies to this model's provider. */
|
|
3919
|
+
function experimentalBetasDisabled(model: Model<"anthropic-messages">): boolean {
|
|
3920
|
+
return model.compat.honorsClaudeCodeBetaKillSwitch === true && $flag("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS");
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
/**
|
|
3924
|
+
* Whether a request body carries a field the kill switch withholds. A frozen
|
|
3925
|
+
* fallback-credit body that does cannot be redeemed under the switch. Defined
|
|
3926
|
+
* as "the sanitizer would change it" so detection and stripping cannot drift.
|
|
3927
|
+
*/
|
|
3928
|
+
function carriesKillSwitchField(params: MessageCreateParamsStreaming): boolean {
|
|
3929
|
+
const sanitized = structuredClone(params);
|
|
3930
|
+
withholdKillSwitchFields(sanitized);
|
|
3931
|
+
return !Bun.deepEquals(sanitized, params);
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
/**
|
|
3935
|
+
* Remove every field the kill switch withholds from a final request body, for
|
|
3936
|
+
* payloads an `onPayload` hook replaced after compat already shaped them, and
|
|
3937
|
+
* for the switch's fields compat has no axis for (task budgets). Compaction
|
|
3938
|
+
* blocks keep their readable summary as text, the same form an endpoint
|
|
3939
|
+
* without compaction support receives.
|
|
3940
|
+
*/
|
|
3941
|
+
function withholdKillSwitchFields(params: MessageCreateParamsStreaming): void {
|
|
3942
|
+
withholdKillSwitchControls(params);
|
|
3943
|
+
delete params.context_management;
|
|
3944
|
+
delete params.compaction;
|
|
3945
|
+
// Server-side fallback entries carry their own per-attempt controls.
|
|
3946
|
+
for (const entry of params.fallbacks ?? []) withholdKillSwitchControls(entry);
|
|
3947
|
+
for (const tool of params.tools ?? []) {
|
|
3948
|
+
if ("strict" in tool) delete tool.strict;
|
|
3949
|
+
if ("defer_loading" in tool) delete tool.defer_loading;
|
|
3950
|
+
if ("eager_input_streaming" in tool) delete tool.eager_input_streaming;
|
|
3951
|
+
}
|
|
3952
|
+
for (const message of params.messages) {
|
|
3953
|
+
// Per-message `output_config` (mid-conversation effort) may carry the same controls.
|
|
3954
|
+
if ("output_config" in message && message.output_config) withholdKillSwitchControls(message);
|
|
3955
|
+
if (!Array.isArray(message.content)) continue;
|
|
3956
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
3957
|
+
const block = message.content[i];
|
|
3958
|
+
if (block.type === "compaction") message.content[i] = { type: "text", text: block.content };
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
if (params.anthropic_beta) {
|
|
3962
|
+
params.anthropic_beta = params.anthropic_beta.filter(beta => killSwitchBetas[beta] !== true);
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
/** Withhold the kill switch's thinking/output controls on a body, fallback entry, or message. */
|
|
3967
|
+
function withholdKillSwitchControls(holder: { thinking?: ThinkingConfigParam; output_config?: OutputConfig }): void {
|
|
3968
|
+
if (holder.thinking && "block_binding" in holder.thinking) delete holder.thinking.block_binding;
|
|
3969
|
+
if (!holder.output_config) return;
|
|
3970
|
+
delete holder.output_config.task_budget;
|
|
3971
|
+
delete holder.output_config.format;
|
|
3972
|
+
if (Object.keys(holder.output_config).length === 0) delete holder.output_config;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
/**
|
|
3976
|
+
* Honor Claude Code's `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` kill switch on
|
|
3977
|
+
* providers whose compat opts in (`honors-claude-code-beta-kill-switch`).
|
|
3978
|
+
* Gateways behind `ANTHROPIC_BASE_URL` often reject pre-release body fields,
|
|
3979
|
+
* so the switch withholds what Claude Code's gateway contract lists — context
|
|
3980
|
+
* management, beta tool fields (`strict`, `defer_loading`,
|
|
3981
|
+
* `eager_input_streaming`), tool search (mid-conversation tool changes),
|
|
3982
|
+
* structured-output format, task budgets — plus `thinking.block_binding` and
|
|
3983
|
+
* server compaction (whose legacy replay needs a `context_management` edit),
|
|
3984
|
+
* together with their betas. Effort, adaptive thinking, interleaved thinking
|
|
3985
|
+
* and extended context stay, as in Claude Code. Applied through compat so every
|
|
3986
|
+
* request builder and retry rung sees one decision; {@link withholdKillSwitchFields}
|
|
3987
|
+
* and {@link killSwitchBetas} cover the rest where bodies and betas are finalized.
|
|
3988
|
+
*/
|
|
3989
|
+
function withoutDisabledExperimentalBetas(model: Model<"anthropic-messages">): Model<"anthropic-messages"> {
|
|
3990
|
+
if (!experimentalBetasDisabled(model)) return model;
|
|
3991
|
+
return {
|
|
3992
|
+
...model,
|
|
3993
|
+
compat: {
|
|
3994
|
+
...model.compat,
|
|
3995
|
+
supportsContextManagement: false,
|
|
3996
|
+
supportsThinkingBindingControls: false,
|
|
3997
|
+
supportsServerCompaction: false,
|
|
3998
|
+
disableStrictTools: true,
|
|
3999
|
+
supportsEagerToolInputStreaming: false,
|
|
4000
|
+
supportsMidConversationToolChanges: false,
|
|
4001
|
+
},
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4010
4005
|
export type AnthropicSystemBlock = {
|
|
4011
4006
|
type: "text";
|
|
4012
4007
|
text: string;
|
|
@@ -4091,6 +4086,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
4091
4086
|
headers,
|
|
4092
4087
|
dynamicHeaders,
|
|
4093
4088
|
hasTools = false,
|
|
4089
|
+
hasBufferedInputTools = false,
|
|
4094
4090
|
thinkingEnabled = false,
|
|
4095
4091
|
isOAuth,
|
|
4096
4092
|
maxRetryDelayMs,
|
|
@@ -4129,8 +4125,9 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
4129
4125
|
!hostMatchesUrl(baseUrl, "githubCopilot")));
|
|
4130
4126
|
const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
|
|
4131
4127
|
const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
|
|
4128
|
+
// The beta streams every tool eagerly, so it is skipped when a tool needs buffered input.
|
|
4132
4129
|
const needsFineGrainedToolStreamingBeta =
|
|
4133
|
-
hasTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming;
|
|
4130
|
+
hasTools && !hasBufferedInputTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming;
|
|
4134
4131
|
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model, baseUrl);
|
|
4135
4132
|
const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
|
|
4136
4133
|
// Disable Bun's native ~300s pre-response fetch timeout (issue #2422).
|
|
@@ -4138,7 +4135,10 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
4138
4135
|
// per request, so the native ceiling can only short-circuit slow-prefill
|
|
4139
4136
|
// streams before the configured watchdog gets to govern them.
|
|
4140
4137
|
const fetchOptions: AnthropicFetchOptions = { ...tlsFetchOptions, timeout: false };
|
|
4141
|
-
|
|
4138
|
+
// Compression sits beneath the cch rewriter, which must hash the uncompressed
|
|
4139
|
+
// body. Catalog rules scope it to verified hosts (official API, Copilot), so
|
|
4140
|
+
// proxies pass through.
|
|
4141
|
+
const baseFetch = wrapFetchForRequestCompression(args.fetch ?? fetch, model);
|
|
4142
4142
|
// Only OAuth requests inject the CC billing header; no API-key request can ever
|
|
4143
4143
|
// contain it, so there is no need to install the rewriter for those.
|
|
4144
4144
|
const cchFetch = oauthToken ? wrapFetchForCch(baseFetch) : baseFetch;
|
|
@@ -4163,7 +4163,9 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
4163
4163
|
// Unlike the generic builder below, this branch has no enforced-key
|
|
4164
4164
|
// filter: a caller-supplied `anthropic-beta` lands in the default
|
|
4165
4165
|
// headers verbatim, cache betas included.
|
|
4166
|
-
const defaultHeaders = dropPromptCacheBetas
|
|
4166
|
+
const defaultHeaders = dropPromptCacheBetas
|
|
4167
|
+
? stripHeaderBetas(mergedCopilotHeaders, promptCacheBetas)
|
|
4168
|
+
: mergedCopilotHeaders;
|
|
4167
4169
|
applyInferenceHeaders(defaultHeaders, {
|
|
4168
4170
|
provider: model.provider,
|
|
4169
4171
|
protocol: "anthropic",
|
|
@@ -4208,9 +4210,13 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
4208
4210
|
// value outright — but `allowAnthropicHeaderOverrides` lets it replace our
|
|
4209
4211
|
// built beta header wholesale, which would put a cache beta back on a
|
|
4210
4212
|
// breakpoint-free request.
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
+
// The kill switch's betas are stripped on the same channel.
|
|
4214
|
+
const cacheStrippedHeaders = dropPromptCacheBetas
|
|
4215
|
+
? stripHeaderBetas(mergedRequestHeaders, promptCacheBetas)
|
|
4213
4216
|
: mergedRequestHeaders;
|
|
4217
|
+
const requestModelHeaders = experimentalBetasDisabled(model)
|
|
4218
|
+
? stripHeaderBetas(cacheStrippedHeaders, killSwitchBetas)
|
|
4219
|
+
: cacheStrippedHeaders;
|
|
4214
4220
|
const defaultHeaders = buildAnthropicHeaders({
|
|
4215
4221
|
apiKey,
|
|
4216
4222
|
baseUrl,
|
|
@@ -4298,6 +4304,20 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
4298
4304
|
};
|
|
4299
4305
|
}
|
|
4300
4306
|
|
|
4307
|
+
/**
|
|
4308
|
+
* Warm-up target for a first-party `anthropic` model: the effective official
|
|
4309
|
+
* API URL plus the TLS options real requests use (sockets pool per TLS
|
|
4310
|
+
* configuration, so a warm-up made with other options is never reused).
|
|
4311
|
+
* `undefined` when requests resolve elsewhere — `ANTHROPIC_BASE_URL` or
|
|
4312
|
+
* Foundry gateways never receive unsolicited probes.
|
|
4313
|
+
*/
|
|
4314
|
+
export function resolveAnthropicPrewarmTarget(model: Model<Api>): { url: string; init: RequestInit } | undefined {
|
|
4315
|
+
if (!isAnthropicMessagesModel(model)) return undefined;
|
|
4316
|
+
const baseUrl = resolveAnthropicBaseUrl(model) ?? "https://api.anthropic.com";
|
|
4317
|
+
if (!isOfficialAnthropicApiUrl(baseUrl)) return undefined;
|
|
4318
|
+
return { url: baseUrl, init: { ...buildClaudeCodeTlsFetchOptions(model, baseUrl) } };
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4301
4321
|
function createClient(
|
|
4302
4322
|
model: Model<"anthropic-messages">,
|
|
4303
4323
|
args: AnthropicClientOptionsArgs,
|
|
@@ -5144,7 +5164,24 @@ type AnthropicParamBuildOptions = {
|
|
|
5144
5164
|
* priority-tier request to first-party Anthropic.
|
|
5145
5165
|
*/
|
|
5146
5166
|
function wantsAnthropicFastMode(model: Model<"anthropic-messages">, options: AnthropicOptions | undefined): boolean {
|
|
5147
|
-
return model.compat.fastMode === true || (model.
|
|
5167
|
+
return model.compat.fastMode === true || (model.compat.firstPartyProvider && options?.serviceTier === "priority");
|
|
5168
|
+
}
|
|
5169
|
+
|
|
5170
|
+
/**
|
|
5171
|
+
* The block that decides whether an assistant turn is thinking-led. A
|
|
5172
|
+
* `fallback` boundary is bookkeeping, not model output: a response that fell
|
|
5173
|
+
* back before the primary emitted any reasoning opens with
|
|
5174
|
+
* `[fallback, thinking(fallbackModel), …]` and is still a thinking-led turn,
|
|
5175
|
+
* so treat the marker as transparent. Tolerates malformed blocks replayed off
|
|
5176
|
+
* disk.
|
|
5177
|
+
*/
|
|
5178
|
+
function thinkingLedLeadBlock(content: readonly unknown[]): { type?: unknown } | undefined {
|
|
5179
|
+
for (const block of content) {
|
|
5180
|
+
if (block == null || typeof block !== "object") return undefined;
|
|
5181
|
+
const typed = block as { type?: unknown };
|
|
5182
|
+
if (typed.type !== "fallback") return typed;
|
|
5183
|
+
}
|
|
5184
|
+
return undefined;
|
|
5148
5185
|
}
|
|
5149
5186
|
|
|
5150
5187
|
/**
|
|
@@ -5158,14 +5195,8 @@ function hasThinkinglessAssistantHistory(messages: readonly Message[]): boolean
|
|
|
5158
5195
|
if (message.role !== "assistant") continue;
|
|
5159
5196
|
if (!Array.isArray(message.content) || message.content.length === 0) continue;
|
|
5160
5197
|
hasAssistant = true;
|
|
5161
|
-
const first = message.content
|
|
5162
|
-
if (
|
|
5163
|
-
first != null &&
|
|
5164
|
-
typeof first === "object" &&
|
|
5165
|
-
(first.type === "thinking" || first.type === "redactedThinking")
|
|
5166
|
-
) {
|
|
5167
|
-
return false;
|
|
5168
|
-
}
|
|
5198
|
+
const first = thinkingLedLeadBlock(message.content);
|
|
5199
|
+
if (first?.type === "thinking" || first?.type === "redactedThinking") return false;
|
|
5169
5200
|
}
|
|
5170
5201
|
return hasAssistant;
|
|
5171
5202
|
}
|
|
@@ -5188,8 +5219,8 @@ function hasNonThinkingTurnAfterLastUser(messages: readonly Message[]): boolean
|
|
|
5188
5219
|
const next = messages[lastUser + 1];
|
|
5189
5220
|
if (next.role !== "assistant") return false;
|
|
5190
5221
|
if (!Array.isArray(next.content) || next.content.length === 0) return false;
|
|
5191
|
-
const first = next.content
|
|
5192
|
-
if (first
|
|
5222
|
+
const first = thinkingLedLeadBlock(next.content);
|
|
5223
|
+
if (first === undefined) return false;
|
|
5193
5224
|
return first.type !== "thinking" && first.type !== "redactedThinking";
|
|
5194
5225
|
}
|
|
5195
5226
|
|
|
@@ -5700,14 +5731,46 @@ function toWellFormedDeep(value: unknown): unknown {
|
|
|
5700
5731
|
return value;
|
|
5701
5732
|
}
|
|
5702
5733
|
|
|
5734
|
+
/**
|
|
5735
|
+
* Collapse a server-side-fallback turn for a request that cannot carry the
|
|
5736
|
+
* `fallback` boundary marker: drop the markers and keep reasoning from one
|
|
5737
|
+
* model only — the last segment that has any. Each model signed its reasoning
|
|
5738
|
+
* against the turn's original prefix; replayed back-to-back without the
|
|
5739
|
+
* marker, Anthropic validates them as one chain and rejects the latest turn
|
|
5740
|
+
* with "`thinking` or `redacted_thinking` blocks in the latest assistant
|
|
5741
|
+
* message cannot be modified". A turn whose reasoning sits in one segment
|
|
5742
|
+
* keeps all of it.
|
|
5743
|
+
*/
|
|
5744
|
+
function collapseFallbackSegments(content: AssistantMessage["content"]): AssistantMessage["content"] {
|
|
5745
|
+
let segments = 0;
|
|
5746
|
+
let reasoningSegment = 0;
|
|
5747
|
+
for (const block of content) {
|
|
5748
|
+
if (block.type === "fallback") segments++;
|
|
5749
|
+
else if (block.type === "thinking" || block.type === "redactedThinking") reasoningSegment = segments;
|
|
5750
|
+
}
|
|
5751
|
+
if (segments === 0) return content;
|
|
5752
|
+
const collapsed: AssistantMessage["content"] = [];
|
|
5753
|
+
let segment = 0;
|
|
5754
|
+
for (const block of content) {
|
|
5755
|
+
if (block.type === "fallback") {
|
|
5756
|
+
segment++;
|
|
5757
|
+
continue;
|
|
5758
|
+
}
|
|
5759
|
+
if ((block.type === "thinking" || block.type === "redactedThinking") && segment !== reasoningSegment) continue;
|
|
5760
|
+
collapsed.push(block);
|
|
5761
|
+
}
|
|
5762
|
+
return collapsed;
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5703
5765
|
/**
|
|
5704
5766
|
* Serialize zeta {@link Message}s to Anthropic wire messages.
|
|
5705
5767
|
*
|
|
5706
5768
|
* `opts.serverSideFallbackEnabled` — when the CURRENT request itself
|
|
5707
|
-
* opts into the server-side-fallback beta chain. Only then
|
|
5708
|
-
* `fallback` content block from a prior
|
|
5709
|
-
* otherwise the block is dropped to avoid a 400
|
|
5710
|
-
* that don't send the beta
|
|
5769
|
+
* opts into the server-side-fallback beta chain. Only then, and only on the
|
|
5770
|
+
* official endpoint, may a persisted `fallback` content block from a prior
|
|
5771
|
+
* turn be replayed on the wire; otherwise the block is dropped to avoid a 400
|
|
5772
|
+
* on requests that don't send the beta, and its turn keeps one model's
|
|
5773
|
+
* reasoning (see {@link collapseFallbackSegments}).
|
|
5711
5774
|
*
|
|
5712
5775
|
* `opts.replayCompaction` — replay a user-role compaction summary that
|
|
5713
5776
|
* carries an {@link AnthropicCompactionPayload} from this provider as a
|
|
@@ -5731,9 +5794,9 @@ export function convertAnthropicMessages(
|
|
|
5731
5794
|
compactionRequest?: AnthropicCompactionRequest;
|
|
5732
5795
|
},
|
|
5733
5796
|
): AnthropicMessageParam[] {
|
|
5734
|
-
// Indices of params emitted from `developer` messages. After the
|
|
5735
|
-
// the ones whose placement satisfies Anthropic's mid-conversation
|
|
5736
|
-
// upgraded from the `user` role to the authoritative `system` role.
|
|
5797
|
+
// Indices of params emitted from agent-owned `developer` messages. After the
|
|
5798
|
+
// main pass, the ones whose placement satisfies Anthropic's mid-conversation
|
|
5799
|
+
// rules are upgraded from the `user` role to the authoritative `system` role.
|
|
5737
5800
|
const developerParams: Array<{ index: number; payload?: AnthropicMessagePayload }> = [];
|
|
5738
5801
|
const params: AnthropicMessageParam[] = [];
|
|
5739
5802
|
// Controls rendered from request-control markers. The assistant repairs
|
|
@@ -5772,7 +5835,7 @@ export function convertAnthropicMessages(
|
|
|
5772
5835
|
const transformedMessages = transformMessages(
|
|
5773
5836
|
messages,
|
|
5774
5837
|
model,
|
|
5775
|
-
normalizeToolCallId,
|
|
5838
|
+
id => normalizeToolCallId(id),
|
|
5776
5839
|
undefined,
|
|
5777
5840
|
undefined,
|
|
5778
5841
|
undefined,
|
|
@@ -5860,7 +5923,13 @@ export function convertAnthropicMessages(
|
|
|
5860
5923
|
blocks.push(...anthropicToolChangeBlocks(payload.toolChanges, isOAuthToken, model));
|
|
5861
5924
|
content = blocks;
|
|
5862
5925
|
}
|
|
5863
|
-
|
|
5926
|
+
// User-attributed developer turns (text `@file` mentions, hidden extension
|
|
5927
|
+
// messages) only borrow the slot: their content is the user's, so it stays
|
|
5928
|
+
// on `user` instead of gaining system authority or ending the request in
|
|
5929
|
+
// place of the user's turn. Provider controls only exist on `system`.
|
|
5930
|
+
if (msg.role === "developer" && (msg.attribution !== "user" || hasProviderControls)) {
|
|
5931
|
+
developerParams.push({ index: params.length, payload });
|
|
5932
|
+
}
|
|
5864
5933
|
const param: AnthropicMessageParam & ConversationalUserCarrier = { role: "user", content };
|
|
5865
5934
|
// Record that this wire `user` came from a real conversational turn, so
|
|
5866
5935
|
// prompt-cache decimation can tell it apart from everything else that
|
|
@@ -5876,7 +5945,14 @@ export function convertAnthropicMessages(
|
|
|
5876
5945
|
params.push(param);
|
|
5877
5946
|
} else if (msg.role === "assistant") {
|
|
5878
5947
|
const blocks: ContentBlockParam[] = [];
|
|
5879
|
-
|
|
5948
|
+
// The fallback boundary replays ONLY when both sides are aligned: the
|
|
5949
|
+
// current request opted into the beta chain, and the target is
|
|
5950
|
+
// official Anthropic (the only endpoint that accepts the block).
|
|
5951
|
+
const content =
|
|
5952
|
+
opts?.serverSideFallbackEnabled && model.compat.officialEndpoint
|
|
5953
|
+
? msg.content
|
|
5954
|
+
: collapseFallbackSegments(msg.content);
|
|
5955
|
+
const hasSignedThinking = content.some(
|
|
5880
5956
|
block =>
|
|
5881
5957
|
block.type === "thinking" && !!block.thinkingSignature && block.thinkingSignature.trim().length > 0,
|
|
5882
5958
|
);
|
|
@@ -5894,7 +5970,7 @@ export function convertAnthropicMessages(
|
|
|
5894
5970
|
blocks.push(compactionBlockParam(msg.providerPayload));
|
|
5895
5971
|
}
|
|
5896
5972
|
|
|
5897
|
-
for (const block of
|
|
5973
|
+
for (const block of content) {
|
|
5898
5974
|
if (block.type === "text") {
|
|
5899
5975
|
if (block.text.trim().length === 0) continue;
|
|
5900
5976
|
blocks.push({
|
|
@@ -5955,13 +6031,7 @@ export function convertAnthropicMessages(
|
|
|
5955
6031
|
} else if (block.type === "anthropicServerTool") {
|
|
5956
6032
|
blocks.push(block.block);
|
|
5957
6033
|
} else if (block.type === "fallback") {
|
|
5958
|
-
//
|
|
5959
|
-
// request opted into the beta chain, and the target is
|
|
5960
|
-
// official Anthropic (the only endpoint that accepts the
|
|
5961
|
-
// block on the wire). `transformMessages` already drops
|
|
5962
|
-
// the block for cross-provider / non-official replays, so
|
|
5963
|
-
// this is defense-in-depth for direct convert calls.
|
|
5964
|
-
if (!opts?.serverSideFallbackEnabled || !model.compat.officialEndpoint) continue;
|
|
6034
|
+
// Present only when this request can carry it (see `content`).
|
|
5965
6035
|
blocks.push({
|
|
5966
6036
|
type: "fallback",
|
|
5967
6037
|
from: block.from,
|
|
@@ -6254,10 +6324,20 @@ const ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS = new Set([
|
|
|
6254
6324
|
// Not `bash`: strict decoding fixes property order, so an optional key declared
|
|
6255
6325
|
// before one the model has already written can no longer be emitted, and bash's
|
|
6256
6326
|
// `timeout` vanished from every `async`-first call.
|
|
6257
|
-
|
|
6327
|
+
// `yield` carries a subagent's whole structured result. Its properties (`type`,
|
|
6328
|
+
// `data`, `error`) are declared in the order callers write them; schema-less and
|
|
6329
|
+
// workpool yields report `strict: false`. Strict only holds together with buffered
|
|
6330
|
+
// input streaming (see ANTHROPIC_BUFFERED_INPUT_TOOLS).
|
|
6331
|
+
const ANTHROPIC_STRICT_TOOL_ALLOWLIST = new Set(["python", "edit", "find", "yield"]);
|
|
6258
6332
|
const MAX_ANTHROPIC_STRICT_TOOLS = 20;
|
|
6259
6333
|
const MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS = 24;
|
|
6260
6334
|
const MAX_ANTHROPIC_STRICT_UNION_PARAMETERS = 16;
|
|
6335
|
+
// Tools sent with `eager_input_streaming: false`, so the API buffers and validates
|
|
6336
|
+
// their input. Eagerly streamed `yield` input from Claude Haiku arrived as invalid
|
|
6337
|
+
// JSON (a large nested result closed with one brace too many) even with `strict`
|
|
6338
|
+
// set; buffered, it did not. Where the per-tool flag is unsupported, their presence
|
|
6339
|
+
// drops the request-wide fine-grained streaming beta instead.
|
|
6340
|
+
const ANTHROPIC_BUFFERED_INPUT_TOOLS = new Set(["yield"]);
|
|
6261
6341
|
|
|
6262
6342
|
/** `minItems` / `maxItems` apply to arrays; Anthropic rejects them on `type: "object"` (including `minItems: 0`/`1`). */
|
|
6263
6343
|
function isJsonSchemaArrayNode(schema: Record<string, unknown>): boolean {
|
|
@@ -6721,7 +6801,9 @@ function convertTools(
|
|
|
6721
6801
|
};
|
|
6722
6802
|
return {
|
|
6723
6803
|
...baseTool,
|
|
6724
|
-
...(supportsEagerToolInputStreaming
|
|
6804
|
+
...(supportsEagerToolInputStreaming
|
|
6805
|
+
? { eager_input_streaming: !ANTHROPIC_BUFFERED_INPUT_TOOLS.has(tool.name) }
|
|
6806
|
+
: {}),
|
|
6725
6807
|
...(plan.strict ? { strict: true } : {}),
|
|
6726
6808
|
...(tool.deferLoading ? { defer_loading: true } : {}),
|
|
6727
6809
|
};
|