@gajae-code/ai 0.18.11 → 0.19.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 +103 -0
- package/README.md +1 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
- package/dist/types/auth-storage.d.ts +98 -13
- package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
- package/dist/types/model-cache.d.ts +6 -4
- package/dist/types/model-manager.d.ts +2 -0
- package/dist/types/model-pricing.d.ts +8 -0
- package/dist/types/model-thinking.d.ts +11 -0
- package/dist/types/openai-completions-compat.d.ts +2 -1
- package/dist/types/provider-models/google.d.ts +2 -0
- package/dist/types/providers/amazon-bedrock.d.ts +17 -2
- package/dist/types/providers/anthropic.d.ts +25 -0
- package/dist/types/providers/aws-credential-config.d.ts +5 -0
- package/dist/types/providers/google-auth.d.ts +7 -0
- package/dist/types/providers/google-gemini-headers.d.ts +11 -6
- package/dist/types/providers/kiro-api-key.d.ts +5 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/providers/openai-responses-shared.d.ts +2 -1
- package/dist/types/stream.d.ts +33 -1
- package/dist/types/types.d.ts +26 -3
- package/dist/types/usage.d.ts +3 -0
- package/dist/types/utils/discovery/antigravity.d.ts +16 -1
- package/dist/types/utils/idle-iterator.d.ts +3 -2
- package/dist/types/utils/oauth/glm-zcode.d.ts +0 -30
- package/dist/types/utils/oauth/types.d.ts +9 -0
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +62 -11
- package/src/auth-broker/wire-schemas.ts +2 -0
- package/src/auth-gateway/server.ts +3 -2
- package/src/auth-storage.ts +908 -246
- package/src/bedrock-claude-cache-policy.d.ts +1 -1
- package/src/bedrock-claude-cache-policy.ts +2 -2
- package/src/model-cache.ts +45 -10
- package/src/model-manager.ts +18 -6
- package/src/model-pricing.d.ts +8 -0
- package/src/model-pricing.ts +87 -0
- package/src/model-thinking.d.ts +11 -0
- package/src/model-thinking.ts +97 -12
- package/src/models.json +3176 -1670
- package/src/models.ts +10 -3
- package/src/openai-completions-compat.d.ts +2 -1
- package/src/openai-completions-compat.ts +5 -0
- package/src/provider-models/google.ts +11 -1
- package/src/provider-models/openai-compat.ts +26 -0
- package/src/providers/amazon-bedrock.d.ts +17 -2
- package/src/providers/amazon-bedrock.ts +76 -16
- package/src/providers/anthropic.d.ts +25 -0
- package/src/providers/anthropic.ts +230 -18
- package/src/providers/aws-credential-config.d.ts +5 -0
- package/src/providers/aws-credential-config.ts +29 -1
- package/src/providers/google-auth.d.ts +7 -0
- package/src/providers/google-auth.ts +165 -4
- package/src/providers/google-gemini-headers.d.ts +11 -6
- package/src/providers/google-gemini-headers.ts +16 -11
- package/src/providers/google-vertex.ts +2 -4
- package/src/providers/kiro-api-key.d.ts +5 -0
- package/src/providers/kiro-api-key.ts +379 -68
- package/src/providers/ollama.ts +19 -9
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +14 -2
- package/src/providers/openai-codex-responses.ts +21 -14
- package/src/providers/openai-completions.d.ts +1 -1
- package/src/providers/openai-completions.ts +65 -4
- package/src/providers/openai-responses-server.ts +10 -1
- package/src/providers/openai-responses-shared.d.ts +2 -1
- package/src/providers/openai-responses-shared.ts +42 -1
- package/src/providers/openai-responses.ts +51 -10
- package/src/providers/pi-native-server.ts +1 -1
- package/src/providers/transform-messages.ts +32 -1
- package/src/stream.d.ts +33 -1
- package/src/stream.ts +111 -17
- package/src/types.d.ts +26 -3
- package/src/types.ts +55 -2
- package/src/usage/claude.ts +4 -2
- package/src/usage.ts +3 -0
- package/src/utils/discovery/antigravity.ts +192 -7
- package/src/utils/idle-iterator.d.ts +3 -2
- package/src/utils/idle-iterator.ts +30 -10
- package/src/utils/oauth/anthropic.ts +23 -22
- package/src/utils/oauth/api-key-validation.ts +34 -11
- package/src/utils/oauth/glm-zcode.d.ts +0 -30
- package/src/utils/oauth/glm-zcode.ts +15 -4
- package/src/utils/oauth/openai-codex.ts +41 -6
- package/src/utils/oauth/types.d.ts +9 -0
- package/src/utils/oauth/types.ts +9 -0
- package/src/utils/parse-bind.d.ts +35 -0
- package/src/utils/proxy.ts +35 -6
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import { scheduler } from "node:timers/promises";
|
|
5
5
|
import * as tls from "node:tls";
|
|
6
|
+
import { gzipSync } from "node:zlib";
|
|
6
7
|
import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
|
|
7
8
|
import type {
|
|
8
9
|
ContentBlockParam,
|
|
@@ -34,8 +35,9 @@ import {
|
|
|
34
35
|
} from "../adapter-internals/provider-safety-stop";
|
|
35
36
|
import {
|
|
36
37
|
getMiniMaxThinkingMode,
|
|
37
|
-
|
|
38
|
+
hasAnthropicModelSamplingRestrictions,
|
|
38
39
|
mapEffortToAnthropicAdaptiveEffort,
|
|
40
|
+
requiresAnthropicThinkingDisableWhenOff,
|
|
39
41
|
supportsAnthropicAdaptiveThinkingDisplay as supportsAdaptiveThinkingDisplay,
|
|
40
42
|
} from "../model-thinking";
|
|
41
43
|
import { calculateCost } from "../models";
|
|
@@ -388,6 +390,14 @@ type AnthropicProviderSessionState = ProviderSessionState & {
|
|
|
388
390
|
thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope;
|
|
389
391
|
thinkingReplayRepairAttempts: number;
|
|
390
392
|
thinkingReplayRejectedPayload?: AnthropicPayloadFingerprint;
|
|
393
|
+
/**
|
|
394
|
+
* sha256 digests of base64 image payloads Anthropic rejected with
|
|
395
|
+
* "Could not process image". The image stays in session history, so every
|
|
396
|
+
* later build must keep replacing it or each turn draws the same 400.
|
|
397
|
+
*/
|
|
398
|
+
omittedImageDigests: Set<string>;
|
|
399
|
+
/** Image omissions spent since the last completed stream; bounds escalation. */
|
|
400
|
+
imageOmissionRepairs: number;
|
|
391
401
|
/**
|
|
392
402
|
* Managed-mode escalation for the CPA alias-restore failure (issue #4338):
|
|
393
403
|
* corrective steering recorded against one exact turn, applied by the next
|
|
@@ -416,6 +426,8 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
|
|
|
416
426
|
warnedUnknownLongCacheRetention: new Set(),
|
|
417
427
|
thinkingReplayRepairScope: "none",
|
|
418
428
|
thinkingReplayRepairAttempts: 0,
|
|
429
|
+
omittedImageDigests: new Set(),
|
|
430
|
+
imageOmissionRepairs: 0,
|
|
419
431
|
close: () => {
|
|
420
432
|
state.strictToolsDisabled = false;
|
|
421
433
|
state.fastModeDisabled = false;
|
|
@@ -424,6 +436,8 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
|
|
|
424
436
|
state.thinkingReplayRepairScope = "none";
|
|
425
437
|
state.thinkingReplayRepairAttempts = 0;
|
|
426
438
|
state.thinkingReplayRejectedPayload = undefined;
|
|
439
|
+
state.omittedImageDigests.clear();
|
|
440
|
+
state.imageOmissionRepairs = 0;
|
|
427
441
|
state.cpaToolAliasSteering = undefined;
|
|
428
442
|
},
|
|
429
443
|
};
|
|
@@ -532,6 +546,86 @@ export function isAnthropicThinkingSignatureInvalidError(error: unknown): boolea
|
|
|
532
546
|
);
|
|
533
547
|
}
|
|
534
548
|
|
|
549
|
+
/**
|
|
550
|
+
* 400 shape where Anthropic cannot decode one of the request's images:
|
|
551
|
+
* `{"type":"invalid_request_error","message":"Could not process image"}`.
|
|
552
|
+
* Observed for a truncated JPEG (no end-of-image marker) that local resizing
|
|
553
|
+
* also failed to decode and therefore forwarded unchanged. The body names no
|
|
554
|
+
* image, and the image stays in history, so without omission every later
|
|
555
|
+
* request of the session draws the same rejection.
|
|
556
|
+
*/
|
|
557
|
+
export function isAnthropicImageUnprocessableError(error: unknown): boolean {
|
|
558
|
+
if (!isAnthropicInvalidRequestStatus(error)) return false;
|
|
559
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
560
|
+
return /invalid_request_error/i.test(message) && /could not process image/i.test(message);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Bounds image omission per stream: the first rejection omits the images of the
|
|
565
|
+
* newest message that carries any (the request before it was accepted, so the
|
|
566
|
+
* rejected image is almost always new), the second omits every remaining image.
|
|
567
|
+
*/
|
|
568
|
+
const ANTHROPIC_MAX_IMAGE_OMISSION_REPAIRS = 2;
|
|
569
|
+
|
|
570
|
+
export const ANTHROPIC_OMITTED_IMAGE_PLACEHOLDER =
|
|
571
|
+
"[Image omitted: Anthropic could not process this image. The file may be corrupt or truncated; re-export or convert it before attaching it again.]";
|
|
572
|
+
|
|
573
|
+
type AnthropicImageSite = {
|
|
574
|
+
digest: string;
|
|
575
|
+
messageIndex: number;
|
|
576
|
+
container: unknown[];
|
|
577
|
+
index: number;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
function collectAnthropicImageSites(params: MessageCreateParamsStreaming): AnthropicImageSite[] {
|
|
581
|
+
const sites: AnthropicImageSite[] = [];
|
|
582
|
+
const visit = (container: unknown[], messageIndex: number): void => {
|
|
583
|
+
for (let index = 0; index < container.length; index++) {
|
|
584
|
+
const block = container[index];
|
|
585
|
+
if (!isRecord(block)) continue;
|
|
586
|
+
if (block.type === "tool_result" && Array.isArray(block.content)) {
|
|
587
|
+
visit(block.content, messageIndex);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (block.type !== "image" || !isRecord(block.source)) continue;
|
|
591
|
+
const data = block.source.data;
|
|
592
|
+
if (typeof data !== "string") continue;
|
|
593
|
+
const digest = nodeCrypto.createHash("sha256").update(data).digest("hex");
|
|
594
|
+
sites.push({ digest, messageIndex, container, index });
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
params.messages.forEach((message, messageIndex) => {
|
|
598
|
+
if (Array.isArray(message.content)) visit(message.content as unknown[], messageIndex);
|
|
599
|
+
});
|
|
600
|
+
return sites;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Replace every image whose payload digest is listed, keeping its cache breakpoint. */
|
|
604
|
+
function omitAnthropicImages(params: MessageCreateParamsStreaming, digests: ReadonlySet<string>): void {
|
|
605
|
+
for (const site of collectAnthropicImageSites(params)) {
|
|
606
|
+
if (!digests.has(site.digest)) continue;
|
|
607
|
+
const image = site.container[site.index] as Record<string, unknown>;
|
|
608
|
+
site.container[site.index] = {
|
|
609
|
+
type: "text",
|
|
610
|
+
text: ANTHROPIC_OMITTED_IMAGE_PLACEHOLDER,
|
|
611
|
+
...(image.cache_control === undefined ? {} : { cache_control: image.cache_control }),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Digests to omit after a rejection: the newest image-bearing message first, then everything. */
|
|
617
|
+
function selectAnthropicImagesToOmit(
|
|
618
|
+
params: MessageCreateParamsStreaming,
|
|
619
|
+
omitted: ReadonlySet<string>,
|
|
620
|
+
scope: "latest" | "all",
|
|
621
|
+
): string[] {
|
|
622
|
+
const remaining = collectAnthropicImageSites(params).filter(site => !omitted.has(site.digest));
|
|
623
|
+
if (remaining.length === 0) return [];
|
|
624
|
+
const newest = Math.max(...remaining.map(site => site.messageIndex));
|
|
625
|
+
const selected = scope === "latest" ? remaining.filter(site => site.messageIndex === newest) : remaining;
|
|
626
|
+
return [...new Set(selected.map(site => site.digest))];
|
|
627
|
+
}
|
|
628
|
+
|
|
535
629
|
/**
|
|
536
630
|
* CLIProxyAPI replaces Anthropic's rejection body wholesale instead of forwarding
|
|
537
631
|
* it: the client only ever sees
|
|
@@ -1494,6 +1588,51 @@ function wrapAnthropicFetchForBoundedRateLimits(baseFetch: FetchImpl, maxRetryDe
|
|
|
1494
1588
|
);
|
|
1495
1589
|
}
|
|
1496
1590
|
|
|
1591
|
+
/** Body bytes for a request `BodyInit`, or `undefined` when the body cannot be inspected. */
|
|
1592
|
+
function requestBodyBuffer(body: RequestInit["body"]): Buffer | undefined {
|
|
1593
|
+
if (typeof body === "string") return Buffer.from(body, "utf-8");
|
|
1594
|
+
if (body instanceof ArrayBuffer) return Buffer.from(body);
|
|
1595
|
+
if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
1596
|
+
return undefined;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/** Compresses request bodies with gzip for first-party Anthropic endpoints only. */
|
|
1600
|
+
export function wrapAnthropicFetchForGzip(baseFetch: FetchImpl): FetchImpl {
|
|
1601
|
+
return Object.assign(
|
|
1602
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
1603
|
+
const url = new URL(input instanceof Request ? input.url : String(input), "https://api.anthropic.com");
|
|
1604
|
+
const isFirstPartyAnthropicApi =
|
|
1605
|
+
url.protocol.toLowerCase() === "https:" &&
|
|
1606
|
+
url.hostname.toLowerCase() === "api.anthropic.com" &&
|
|
1607
|
+
(url.port === "" || url.port === "443");
|
|
1608
|
+
const bodyBuffer = init?.body ? requestBodyBuffer(init.body) : undefined;
|
|
1609
|
+
if (!isFirstPartyAnthropicApi || !bodyBuffer) return baseFetch(input, init);
|
|
1610
|
+
|
|
1611
|
+
let compressedBody: Buffer;
|
|
1612
|
+
try {
|
|
1613
|
+
compressedBody = gzipSync(bodyBuffer);
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
logger.debug("anthropic: gzip compression failed, falling back to uncompressed", {
|
|
1616
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1617
|
+
});
|
|
1618
|
+
return baseFetch(input, init);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// The caller's Content-Length describes the uncompressed body; drop it so
|
|
1622
|
+
// fetch recomputes the length for the compressed payload.
|
|
1623
|
+
const headers = new Headers(init?.headers ?? {});
|
|
1624
|
+
headers.set("Content-Encoding", "gzip");
|
|
1625
|
+
headers.delete("Content-Length");
|
|
1626
|
+
logger.debug("anthropic: gzip compression", {
|
|
1627
|
+
uncompressed: bodyBuffer.length,
|
|
1628
|
+
compressed: compressedBody.length,
|
|
1629
|
+
});
|
|
1630
|
+
return baseFetch(input, { ...init, body: compressedBody, headers });
|
|
1631
|
+
},
|
|
1632
|
+
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1497
1636
|
// The Anthropic SDK logs malformed SSE frames directly before rethrowing them.
|
|
1498
1637
|
// We surface the resulting provider error ourselves, so keep the SDK quiet.
|
|
1499
1638
|
const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
|
|
@@ -1640,6 +1779,7 @@ function getAnthropicCompat(
|
|
|
1640
1779
|
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
|
|
1641
1780
|
supportsToolChoice: model.compat?.supportsToolChoice ?? true,
|
|
1642
1781
|
supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
|
|
1782
|
+
supportsUltrafastTier: model.compat?.supportsUltrafastTier ?? false,
|
|
1643
1783
|
promptCacheMode: model.compat?.promptCacheMode ?? "none",
|
|
1644
1784
|
toolChoiceSupport: model.compat?.toolChoiceSupport,
|
|
1645
1785
|
};
|
|
@@ -2095,6 +2235,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2095
2235
|
let activeAbortTracker = createAbortSourceTracker(options?.signal);
|
|
2096
2236
|
|
|
2097
2237
|
try {
|
|
2238
|
+
const idleTimeoutMs =
|
|
2239
|
+
options?.streamIdleTimeoutMs ??
|
|
2240
|
+
getStreamIdleTimeoutMs(model.streamIdleTimeoutMs ?? getProviderStreamIdleTimeoutFallbackMs(model.provider));
|
|
2098
2241
|
let client: Anthropic;
|
|
2099
2242
|
let isOAuthToken: boolean;
|
|
2100
2243
|
|
|
@@ -2127,7 +2270,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2127
2270
|
requestMaxRetries: options?.requestMaxRetries,
|
|
2128
2271
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
2129
2272
|
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
2130
|
-
streamIdleTimeoutMs:
|
|
2273
|
+
streamIdleTimeoutMs: idleTimeoutMs,
|
|
2131
2274
|
providerSessionId: options?.providerSessionId,
|
|
2132
2275
|
});
|
|
2133
2276
|
client = created.client;
|
|
@@ -2154,6 +2297,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2154
2297
|
let thinkingReplayRepairPersistent = thinkingReplayRepairScope !== "none";
|
|
2155
2298
|
let generatedCacheBudget: GeneratedCacheBudget = providerSessionState?.generatedCacheBudget ?? 2;
|
|
2156
2299
|
let warnedUnknownLongCacheRetention = false;
|
|
2300
|
+
// Shared with the session so later turns keep omitting a rejected image
|
|
2301
|
+
// that is still in history; per-stream when no session state is shared.
|
|
2302
|
+
const omittedImageDigests = providerSessionState?.omittedImageDigests ?? new Set<string>();
|
|
2303
|
+
let imageOmissionRepairs = providerSessionState?.imageOmissionRepairs ?? 0;
|
|
2157
2304
|
const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
|
|
2158
2305
|
// Degradation state is cumulative: every fallback rebuild must merge all
|
|
2159
2306
|
// repairs activated so far. Rebuilding from only the immediate call lets
|
|
@@ -2189,6 +2336,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2189
2336
|
}
|
|
2190
2337
|
if (droppedForcedToolChoice) {
|
|
2191
2338
|
delete nextParams.tool_choice;
|
|
2339
|
+
if (options?.thinkingEnabled === false && requiresAnthropicThinkingDisableWhenOff(model.id)) {
|
|
2340
|
+
nextParams.thinking = { type: "disabled" };
|
|
2341
|
+
}
|
|
2192
2342
|
}
|
|
2193
2343
|
if (disableStrictTools) {
|
|
2194
2344
|
dropAnthropicStrictTools(nextParams);
|
|
@@ -2196,6 +2346,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2196
2346
|
if (dropFastMode) {
|
|
2197
2347
|
dropAnthropicFastMode(nextParams);
|
|
2198
2348
|
}
|
|
2349
|
+
if (omittedImageDigests.size > 0) {
|
|
2350
|
+
omitAnthropicImages(nextParams, omittedImageDigests);
|
|
2351
|
+
}
|
|
2199
2352
|
const replacementPayload = await options?.onPayload?.(
|
|
2200
2353
|
nextParams,
|
|
2201
2354
|
model,
|
|
@@ -2315,9 +2468,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2315
2468
|
firstTokenTime = undefined;
|
|
2316
2469
|
truncatedToolCalls.clear();
|
|
2317
2470
|
};
|
|
2318
|
-
const idleTimeoutMs =
|
|
2319
|
-
options?.streamIdleTimeoutMs ??
|
|
2320
|
-
getStreamIdleTimeoutMs(getProviderStreamIdleTimeoutFallbackMs(model.provider));
|
|
2321
2471
|
const firstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(model.provider);
|
|
2322
2472
|
const firstEventTimeoutMs =
|
|
2323
2473
|
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
|
|
@@ -2786,6 +2936,11 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2786
2936
|
providerSessionState.thinkingReplayRepairScope = "none";
|
|
2787
2937
|
providerSessionState.thinkingReplayRepairAttempts = 0;
|
|
2788
2938
|
}
|
|
2939
|
+
// Omitted digests stay recorded (the images are still in history); only
|
|
2940
|
+
// the escalation budget re-arms for a different image rejected later.
|
|
2941
|
+
if (providerSessionState && imageOmissionRepairs > 0) {
|
|
2942
|
+
providerSessionState.imageOmissionRepairs = 0;
|
|
2943
|
+
}
|
|
2789
2944
|
// Release a recorded CPA steering once any stream completes: a
|
|
2790
2945
|
// successful stream consumed it, and a failed one ends the turn (a
|
|
2791
2946
|
// later turn's different user prompt would expire it anyway).
|
|
@@ -2889,6 +3044,36 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
2889
3044
|
resetOutputForRetry();
|
|
2890
3045
|
continue;
|
|
2891
3046
|
}
|
|
3047
|
+
// An image Anthropic cannot decode fails the whole request and stays in
|
|
3048
|
+
// history, so the session never recovers on its own. Record the omission
|
|
3049
|
+
// before deciding who retries: unmanaged streams resend here, managed
|
|
3050
|
+
// attempts leave the retry to the fallback controller, whose next attempt
|
|
3051
|
+
// rebuilds from the shared session state without the rejected image.
|
|
3052
|
+
if (
|
|
3053
|
+
firstTokenTime === undefined &&
|
|
3054
|
+
imageOmissionRepairs < ANTHROPIC_MAX_IMAGE_OMISSION_REPAIRS &&
|
|
3055
|
+
isAnthropicImageUnprocessableError(streamFailure)
|
|
3056
|
+
) {
|
|
3057
|
+
const scope = imageOmissionRepairs === 0 ? "latest" : "all";
|
|
3058
|
+
const digests = selectAnthropicImagesToOmit(params, omittedImageDigests, scope);
|
|
3059
|
+
if (digests.length > 0) {
|
|
3060
|
+
for (const digest of digests) omittedImageDigests.add(digest);
|
|
3061
|
+
imageOmissionRepairs++;
|
|
3062
|
+
if (providerSessionState) providerSessionState.imageOmissionRepairs = imageOmissionRepairs;
|
|
3063
|
+
logger.warn("anthropic: provider could not process an image; omitting it from the request", {
|
|
3064
|
+
model: model.id,
|
|
3065
|
+
scope,
|
|
3066
|
+
omittedImages: digests.length,
|
|
3067
|
+
managed: options?.fallbackManaged === true,
|
|
3068
|
+
});
|
|
3069
|
+
if (!options?.fallbackManaged && !options?.disableProviderRetries) {
|
|
3070
|
+
params = await prepareParams();
|
|
3071
|
+
providerUploadCount++;
|
|
3072
|
+
resetOutputForRetry();
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
2892
3077
|
const thinkingSignatureInvalid = isAnthropicThinkingSignatureInvalidError(streamFailure);
|
|
2893
3078
|
const thinkingBlocksImmutable = isAnthropicThinkingBlockMutationError(streamFailure);
|
|
2894
3079
|
const maskedProxyRejection = isAnthropicMaskedProxyRejection(streamFailure);
|
|
@@ -3342,8 +3527,9 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
3342
3527
|
const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
|
|
3343
3528
|
const baseFetch = args.fetch ?? fetch;
|
|
3344
3529
|
const boundedFetch = wrapAnthropicFetchForBoundedRateLimits(baseFetch, args.maxRetryDelayMs);
|
|
3530
|
+
const gzipFetch = wrapAnthropicFetchForGzip(boundedFetch);
|
|
3345
3531
|
const openCodeGoSessionId = resolveOpenCodeGoSessionId(model, baseUrl, args.providerSessionId, "anthropic");
|
|
3346
|
-
const providerScopedFetch = wrapFetchForOpenCodeGoSession(
|
|
3532
|
+
const providerScopedFetch = wrapFetchForOpenCodeGoSession(gzipFetch, openCodeGoSessionId);
|
|
3347
3533
|
const debugFetch = onSseEvent
|
|
3348
3534
|
? wrapFetchForSseDebug(providerScopedFetch, event => onSseEvent(event, model))
|
|
3349
3535
|
: providerScopedFetch;
|
|
@@ -3479,11 +3665,22 @@ function createClient(
|
|
|
3479
3665
|
* request drops `thinking`/`output_config`. Reports whether the forced-choice branch
|
|
3480
3666
|
* applied so the caller can keep the replayed history consistent with it.
|
|
3481
3667
|
*/
|
|
3482
|
-
function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming): boolean {
|
|
3668
|
+
function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming, modelId: string): boolean {
|
|
3483
3669
|
const toolChoice = params.tool_choice;
|
|
3484
3670
|
if (!toolChoice) return false;
|
|
3485
3671
|
if (toolChoice.type !== "any" && toolChoice.type !== "tool") return false;
|
|
3486
|
-
|
|
3672
|
+
if (requiresAnthropicThinkingDisableWhenOff(modelId)) {
|
|
3673
|
+
// Haiku 5+ enables adaptive thinking when `thinking` is omitted, which a forced tool
|
|
3674
|
+
// choice rejects, and applies `output_config.effort` to all response tokens including
|
|
3675
|
+
// tool-call arguments. Send an explicit `disabled` and keep the selected effort;
|
|
3676
|
+
// effort is capped at `high` while thinking is disabled.
|
|
3677
|
+
params.thinking = { type: "disabled" };
|
|
3678
|
+
const effort = params.output_config?.effort;
|
|
3679
|
+
if (effort === "xhigh" || effort === "max") params.output_config = { ...params.output_config, effort: "high" };
|
|
3680
|
+
return true;
|
|
3681
|
+
}
|
|
3682
|
+
// An explicit `disabled` is compatible with forced tool use and is kept.
|
|
3683
|
+
if (params.thinking?.type !== "disabled") delete params.thinking;
|
|
3487
3684
|
delete params.output_config;
|
|
3488
3685
|
return true;
|
|
3489
3686
|
}
|
|
@@ -3727,7 +3924,7 @@ function applyExplicitPromptCaching(
|
|
|
3727
3924
|
}
|
|
3728
3925
|
}
|
|
3729
3926
|
|
|
3730
|
-
function applyPromptCaching(
|
|
3927
|
+
export function applyPromptCaching(
|
|
3731
3928
|
params: AnthropicCacheParams,
|
|
3732
3929
|
cacheMode: AnthropicCacheMode,
|
|
3733
3930
|
cacheControl?: AnthropicCacheControl,
|
|
@@ -3736,9 +3933,22 @@ function applyPromptCaching(
|
|
|
3736
3933
|
if (!cacheControl || cacheMode === "none" || budget === 0) return;
|
|
3737
3934
|
validateCacheControls(params);
|
|
3738
3935
|
if (cacheMode === "automatic") {
|
|
3739
|
-
//
|
|
3740
|
-
//
|
|
3741
|
-
params.cache_control
|
|
3936
|
+
// A caller layout that already holds four block markers leaves no room for a new root marker;
|
|
3937
|
+
// adding one anyway made validateCacheControls reject the request. An existing root is still replaced.
|
|
3938
|
+
if (params.cache_control || countCacheControlBreakpoints(params) < 4) {
|
|
3939
|
+
params.cache_control = { ...cacheControl };
|
|
3940
|
+
}
|
|
3941
|
+
// #2383's moving breakpoint reasoning holds within a session, not across
|
|
3942
|
+
// sessions: anchor tools + system so new messages can reuse that prefix.
|
|
3943
|
+
if (
|
|
3944
|
+
budget > 1 &&
|
|
3945
|
+
Array.isArray(params.system) &&
|
|
3946
|
+
!params.system.some(block => block.cache_control) &&
|
|
3947
|
+
countCacheControlBreakpoints(params) < 4
|
|
3948
|
+
) {
|
|
3949
|
+
const lastBlock = params.system.at(-1);
|
|
3950
|
+
if (lastBlock) lastBlock.cache_control = { ...cacheControl };
|
|
3951
|
+
}
|
|
3742
3952
|
return;
|
|
3743
3953
|
}
|
|
3744
3954
|
applyExplicitPromptCaching(params, cacheControl, budget);
|
|
@@ -3812,8 +4022,8 @@ function buildParams(
|
|
|
3812
4022
|
seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX ? seqs.slice(0, ANTHROPIC_STOP_SEQUENCES_MAX) : seqs;
|
|
3813
4023
|
}
|
|
3814
4024
|
|
|
3815
|
-
// Opus 4.7+
|
|
3816
|
-
if (
|
|
4025
|
+
// Opus 4.7+ and Haiku 5+ reject non-default sampling parameters.
|
|
4026
|
+
if (hasAnthropicModelSamplingRestrictions(model.id)) {
|
|
3817
4027
|
delete params.top_p;
|
|
3818
4028
|
delete params.top_k;
|
|
3819
4029
|
delete params.temperature;
|
|
@@ -3833,7 +4043,9 @@ function buildParams(
|
|
|
3833
4043
|
}
|
|
3834
4044
|
|
|
3835
4045
|
const miniMaxMode = getMiniMaxThinkingMode(model, baseUrl);
|
|
3836
|
-
if (
|
|
4046
|
+
if (options?.thinkingEnabled === false && requiresAnthropicThinkingDisableWhenOff(model.id)) {
|
|
4047
|
+
params.thinking = { type: "disabled" };
|
|
4048
|
+
} else if (model.reasoning && miniMaxMode === "toggle") {
|
|
3837
4049
|
// MiniMax's Messages endpoint defaults to off and only supports a
|
|
3838
4050
|
// switch. Claude budget_tokens/output_config.effort do not apply.
|
|
3839
4051
|
if (options?.thinkingEnabled !== undefined) {
|
|
@@ -3849,8 +4061,8 @@ function buildParams(
|
|
|
3849
4061
|
|
|
3850
4062
|
const compat = getAnthropicCompat(model);
|
|
3851
4063
|
if (mode === "anthropic-adaptive" && !compat.disableAdaptiveThinking) {
|
|
3852
|
-
//
|
|
3853
|
-
//
|
|
4064
|
+
// Opus 4.7+, Fable, and Haiku 5+ omit adaptive thinking content by
|
|
4065
|
+
// default. Opt into summarized reasoning so thinking deltas keep
|
|
3854
4066
|
// streaming with human-readable content for callers that rely on it.
|
|
3855
4067
|
const adaptive: { type: "adaptive"; display?: AnthropicThinkingDisplay } = { type: "adaptive" };
|
|
3856
4068
|
if (supportsAdaptiveThinkingDisplay(model.id)) {
|
|
@@ -3908,7 +4120,7 @@ function buildParams(
|
|
|
3908
4120
|
// that pair with `thinking`/`redacted_thinking` blocks "cannot be modified", so the
|
|
3909
4121
|
// replay has to degrade in the same rebuild. Runs before the billing/system payload
|
|
3910
4122
|
// snapshot so the attribution hash covers the messages actually sent.
|
|
3911
|
-
if (disableThinkingIfToolChoiceForced(params) && hasNativeThinkingBlocks(params.messages)) {
|
|
4123
|
+
if (disableThinkingIfToolChoiceForced(params, model.id) && hasNativeThinkingBlocks(params.messages)) {
|
|
3912
4124
|
params.messages = convertAnthropicMessages(context.messages, model, isOAuthToken, {
|
|
3913
4125
|
...thinkingRepair,
|
|
3914
4126
|
repairAllAssistantThinking: true,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FetchImpl } from "../types";
|
|
1
2
|
import type { AwsCredentials } from "./aws-sigv4";
|
|
2
3
|
export type AwsIniFile = Record<string, Record<string, string>>;
|
|
3
4
|
export interface AwsCredentialSourceOptions {
|
|
@@ -16,4 +17,8 @@ export declare function classifyAwsProfileCapability(profile: string, credential
|
|
|
16
17
|
export declare function hasResolvableAwsProfileSource(options?: AwsCredentialSourceOptions & {
|
|
17
18
|
onScan?: () => void;
|
|
18
19
|
}, now?: number): boolean;
|
|
20
|
+
/** Probe only the IMDSv2 token and role endpoints; never request role credentials for status. */
|
|
21
|
+
export declare function hasAwsInstanceProfileCredentialSource(options?: {
|
|
22
|
+
fetch?: FetchImpl;
|
|
23
|
+
}): Promise<boolean>;
|
|
19
24
|
export declare function isValidBedrockBearerToken(token: string | undefined): token is string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { $credentialEnv, getTrustedHomeDir } from "@gajae-code/utils";
|
|
3
|
+
import { $credentialEnv, $env, getTrustedHomeDir } from "@gajae-code/utils";
|
|
4
|
+
import type { FetchImpl } from "../types";
|
|
4
5
|
import type { AwsCredentials } from "./aws-sigv4";
|
|
5
6
|
|
|
6
7
|
export type AwsIniFile = Record<string, Record<string, string>>;
|
|
@@ -122,6 +123,33 @@ export function hasResolvableAwsProfileSource(
|
|
|
122
123
|
return value;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/** Probe only the IMDSv2 token and role endpoints; never request role credentials for status. */
|
|
127
|
+
export async function hasAwsInstanceProfileCredentialSource(options?: { fetch?: FetchImpl }): Promise<boolean> {
|
|
128
|
+
if ($env.AWS_EC2_METADATA_DISABLED?.toLowerCase() === "true") return false;
|
|
129
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
130
|
+
const signal = AbortSignal.timeout(1000);
|
|
131
|
+
try {
|
|
132
|
+
const tokenResponse = await fetchImpl("http://169.254.169.254/latest/api/token", {
|
|
133
|
+
method: "PUT",
|
|
134
|
+
headers: { "x-aws-ec2-metadata-token-ttl-seconds": "21600" },
|
|
135
|
+
redirect: "error",
|
|
136
|
+
signal,
|
|
137
|
+
});
|
|
138
|
+
if (!tokenResponse.ok) return false;
|
|
139
|
+
const token = (await tokenResponse.text()).trim();
|
|
140
|
+
if (!token) return false;
|
|
141
|
+
|
|
142
|
+
const rolesResponse = await fetchImpl("http://169.254.169.254/latest/meta-data/iam/security-credentials/", {
|
|
143
|
+
headers: { "x-aws-ec2-metadata-token": token },
|
|
144
|
+
redirect: "error",
|
|
145
|
+
signal,
|
|
146
|
+
});
|
|
147
|
+
return rolesResponse.ok && Boolean((await rolesResponse.text()).trim());
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
125
153
|
export function isValidBedrockBearerToken(token: string | undefined): token is string {
|
|
126
154
|
if (!token) return false;
|
|
127
155
|
return !/[\x00-\x1f\x7f]/.test(token);
|
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
* (default 60s). Concurrent callers waiting on a refresh share the same in-flight promise.
|
|
15
15
|
*/
|
|
16
16
|
import type { FetchImpl } from "../types";
|
|
17
|
+
export type VertexCredentialSource = "explicit-file" | "default-file" | "metadata-server";
|
|
18
|
+
/** Match the region-label constraint used before Vertex interpolates a location into the request host. */
|
|
19
|
+
export declare function isValidVertexLocation(location: string): boolean;
|
|
17
20
|
/** Test seam: the ADC credentials file path as resolved from trusted env. */
|
|
18
21
|
export declare function resolveAdcCredentialsPathForTest(): string | undefined;
|
|
22
|
+
/** Resolve the source of a currently usable ADC credential without acquiring an access token. */
|
|
23
|
+
export declare function getVertexCredentialSource(options?: {
|
|
24
|
+
fetch?: FetchImpl;
|
|
25
|
+
}): Promise<VertexCredentialSource | undefined>;
|
|
19
26
|
/**
|
|
20
27
|
* Returns a Bearer access token suitable for the `Authorization` header on Vertex AI calls.
|
|
21
28
|
* The token is cached in module scope and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` ms before it expires.
|