@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1 -1
  3. package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
  4. package/dist/types/auth-storage.d.ts +98 -13
  5. package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
  6. package/dist/types/model-cache.d.ts +6 -4
  7. package/dist/types/model-manager.d.ts +2 -0
  8. package/dist/types/model-pricing.d.ts +8 -0
  9. package/dist/types/model-thinking.d.ts +11 -0
  10. package/dist/types/openai-completions-compat.d.ts +2 -1
  11. package/dist/types/provider-models/google.d.ts +2 -0
  12. package/dist/types/providers/amazon-bedrock.d.ts +17 -2
  13. package/dist/types/providers/anthropic.d.ts +25 -0
  14. package/dist/types/providers/aws-credential-config.d.ts +5 -0
  15. package/dist/types/providers/google-auth.d.ts +7 -0
  16. package/dist/types/providers/google-gemini-headers.d.ts +11 -6
  17. package/dist/types/providers/kiro-api-key.d.ts +5 -0
  18. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
  19. package/dist/types/providers/openai-completions.d.ts +1 -1
  20. package/dist/types/providers/openai-responses-shared.d.ts +2 -1
  21. package/dist/types/stream.d.ts +33 -1
  22. package/dist/types/types.d.ts +26 -3
  23. package/dist/types/usage.d.ts +3 -0
  24. package/dist/types/utils/discovery/antigravity.d.ts +16 -1
  25. package/dist/types/utils/idle-iterator.d.ts +3 -2
  26. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -30
  27. package/dist/types/utils/oauth/types.d.ts +9 -0
  28. package/package.json +4 -4
  29. package/src/auth-broker/remote-store.ts +62 -11
  30. package/src/auth-broker/wire-schemas.ts +2 -0
  31. package/src/auth-gateway/server.ts +3 -2
  32. package/src/auth-storage.ts +908 -246
  33. package/src/bedrock-claude-cache-policy.d.ts +1 -1
  34. package/src/bedrock-claude-cache-policy.ts +2 -2
  35. package/src/model-cache.ts +45 -10
  36. package/src/model-manager.ts +18 -6
  37. package/src/model-pricing.d.ts +8 -0
  38. package/src/model-pricing.ts +87 -0
  39. package/src/model-thinking.d.ts +11 -0
  40. package/src/model-thinking.ts +97 -12
  41. package/src/models.json +3176 -1670
  42. package/src/models.ts +10 -3
  43. package/src/openai-completions-compat.d.ts +2 -1
  44. package/src/openai-completions-compat.ts +5 -0
  45. package/src/provider-models/google.ts +11 -1
  46. package/src/provider-models/openai-compat.ts +26 -0
  47. package/src/providers/amazon-bedrock.d.ts +17 -2
  48. package/src/providers/amazon-bedrock.ts +76 -16
  49. package/src/providers/anthropic.d.ts +25 -0
  50. package/src/providers/anthropic.ts +230 -18
  51. package/src/providers/aws-credential-config.d.ts +5 -0
  52. package/src/providers/aws-credential-config.ts +29 -1
  53. package/src/providers/google-auth.d.ts +7 -0
  54. package/src/providers/google-auth.ts +165 -4
  55. package/src/providers/google-gemini-headers.d.ts +11 -6
  56. package/src/providers/google-gemini-headers.ts +16 -11
  57. package/src/providers/google-vertex.ts +2 -4
  58. package/src/providers/kiro-api-key.d.ts +5 -0
  59. package/src/providers/kiro-api-key.ts +379 -68
  60. package/src/providers/ollama.ts +19 -9
  61. package/src/providers/openai-anthropic-shim.ts +1 -0
  62. package/src/providers/openai-chat-server-schema.ts +1 -1
  63. package/src/providers/openai-chat-server.ts +14 -2
  64. package/src/providers/openai-codex-responses.ts +21 -14
  65. package/src/providers/openai-completions.d.ts +1 -1
  66. package/src/providers/openai-completions.ts +65 -4
  67. package/src/providers/openai-responses-server.ts +10 -1
  68. package/src/providers/openai-responses-shared.d.ts +2 -1
  69. package/src/providers/openai-responses-shared.ts +42 -1
  70. package/src/providers/openai-responses.ts +51 -10
  71. package/src/providers/pi-native-server.ts +1 -1
  72. package/src/providers/transform-messages.ts +32 -1
  73. package/src/stream.d.ts +33 -1
  74. package/src/stream.ts +111 -17
  75. package/src/types.d.ts +26 -3
  76. package/src/types.ts +55 -2
  77. package/src/usage/claude.ts +4 -2
  78. package/src/usage.ts +3 -0
  79. package/src/utils/discovery/antigravity.ts +192 -7
  80. package/src/utils/idle-iterator.d.ts +3 -2
  81. package/src/utils/idle-iterator.ts +30 -10
  82. package/src/utils/oauth/anthropic.ts +23 -22
  83. package/src/utils/oauth/api-key-validation.ts +34 -11
  84. package/src/utils/oauth/glm-zcode.d.ts +0 -30
  85. package/src/utils/oauth/glm-zcode.ts +15 -4
  86. package/src/utils/oauth/openai-codex.ts +41 -6
  87. package/src/utils/oauth/types.d.ts +9 -0
  88. package/src/utils/oauth/types.ts +9 -0
  89. package/src/utils/parse-bind.d.ts +35 -0
  90. 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
- hasOpus47ApiRestrictions,
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: options?.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(boundedFetch, openCodeGoSessionId);
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
- delete params.thinking;
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
- // Automatic mode only ever emits one marker, so any non-zero budget
3740
- // covers it; the zero case already returned above.
3741
- params.cache_control = { ...cacheControl };
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+ rejects non-default sampling parameters with 400 error.
3816
- if (hasOpus47ApiRestrictions(model.id)) {
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 (model.reasoning && miniMaxMode === "toggle") {
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
- // Starting with Anthropic model Opus 4.7, adaptive thinking content is omitted from the
3853
- // response by default. Opt into summarized reasoning so thinking deltas keep
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.