@gajae-code/ai 0.18.11 → 0.18.12

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 (75) hide show
  1. package/CHANGELOG.md +68 -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 +80 -8
  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/openai-completions-compat.d.ts +2 -1
  9. package/dist/types/providers/anthropic.d.ts +10 -0
  10. package/dist/types/providers/aws-credential-config.d.ts +5 -0
  11. package/dist/types/providers/google-auth.d.ts +7 -0
  12. package/dist/types/providers/google-gemini-headers.d.ts +10 -5
  13. package/dist/types/providers/kiro-api-key.d.ts +5 -0
  14. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
  15. package/dist/types/providers/openai-completions.d.ts +1 -1
  16. package/dist/types/providers/openai-responses-shared.d.ts +2 -1
  17. package/dist/types/stream.d.ts +33 -1
  18. package/dist/types/types.d.ts +26 -3
  19. package/dist/types/usage.d.ts +3 -0
  20. package/dist/types/utils/discovery/antigravity.d.ts +11 -0
  21. package/dist/types/utils/idle-iterator.d.ts +3 -2
  22. package/dist/types/utils/oauth/types.d.ts +9 -0
  23. package/package.json +4 -4
  24. package/src/auth-broker/remote-store.ts +62 -11
  25. package/src/auth-broker/wire-schemas.ts +2 -0
  26. package/src/auth-gateway/server.ts +3 -2
  27. package/src/auth-storage.ts +647 -194
  28. package/src/bedrock-claude-cache-policy.d.ts +1 -1
  29. package/src/bedrock-claude-cache-policy.ts +2 -2
  30. package/src/model-cache.ts +45 -10
  31. package/src/model-manager.ts +18 -6
  32. package/src/model-thinking.ts +37 -7
  33. package/src/models.json +2931 -1670
  34. package/src/openai-completions-compat.d.ts +2 -1
  35. package/src/openai-completions-compat.ts +5 -0
  36. package/src/provider-models/google.ts +7 -1
  37. package/src/provider-models/openai-compat.ts +26 -0
  38. package/src/providers/amazon-bedrock.ts +20 -11
  39. package/src/providers/anthropic.d.ts +10 -0
  40. package/src/providers/anthropic.ts +139 -4
  41. package/src/providers/aws-credential-config.d.ts +5 -0
  42. package/src/providers/aws-credential-config.ts +29 -1
  43. package/src/providers/google-auth.d.ts +7 -0
  44. package/src/providers/google-auth.ts +165 -4
  45. package/src/providers/google-gemini-headers.d.ts +10 -5
  46. package/src/providers/google-gemini-headers.ts +11 -6
  47. package/src/providers/google-vertex.ts +2 -4
  48. package/src/providers/kiro-api-key.d.ts +5 -0
  49. package/src/providers/kiro-api-key.ts +379 -68
  50. package/src/providers/ollama.ts +19 -9
  51. package/src/providers/openai-anthropic-shim.ts +1 -0
  52. package/src/providers/openai-chat-server-schema.ts +1 -1
  53. package/src/providers/openai-chat-server.ts +14 -2
  54. package/src/providers/openai-codex-responses.ts +21 -14
  55. package/src/providers/openai-completions.d.ts +1 -1
  56. package/src/providers/openai-completions.ts +65 -4
  57. package/src/providers/openai-responses-server.ts +10 -1
  58. package/src/providers/openai-responses-shared.d.ts +2 -1
  59. package/src/providers/openai-responses-shared.ts +42 -1
  60. package/src/providers/openai-responses.ts +51 -10
  61. package/src/providers/pi-native-server.ts +1 -1
  62. package/src/stream.d.ts +33 -1
  63. package/src/stream.ts +107 -13
  64. package/src/types.d.ts +26 -3
  65. package/src/types.ts +55 -2
  66. package/src/usage/claude.ts +4 -2
  67. package/src/usage.ts +3 -0
  68. package/src/utils/discovery/antigravity.ts +144 -2
  69. package/src/utils/idle-iterator.d.ts +3 -2
  70. package/src/utils/idle-iterator.ts +30 -10
  71. package/src/utils/oauth/anthropic.ts +23 -22
  72. package/src/utils/oauth/api-key-validation.ts +34 -11
  73. package/src/utils/oauth/types.d.ts +9 -0
  74. package/src/utils/oauth/types.ts +9 -0
  75. package/src/utils/parse-bind.d.ts +35 -0
@@ -27,13 +27,14 @@ export declare function isDirectXaiReasoningEffortModel(model: {
27
27
  api?: string;
28
28
  baseUrl?: string;
29
29
  }, resolvedBaseUrl?: string): boolean;
30
- export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolChoiceSupport" | "supportsResponsesSessionAffinity" | "supportsServiceTier" | "reservedToolNames">> & {
30
+ export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolChoiceSupport" | "supportsResponsesSessionAffinity" | "supportsServiceTier" | "supportsUltrafastTier" | "reservedToolNames">> & {
31
31
  openRouterRouting?: OpenAICompat["openRouterRouting"];
32
32
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
33
33
  extraBody?: OpenAICompat["extraBody"];
34
34
  toolStrictMode: ResolvedToolStrictMode;
35
35
  supportsResponsesSessionAffinity?: OpenAICompat["supportsResponsesSessionAffinity"];
36
36
  supportsServiceTier?: OpenAICompat["supportsServiceTier"];
37
+ supportsUltrafastTier?: OpenAICompat["supportsUltrafastTier"];
37
38
  /** Optional explicit capability override; resolved via deriveToolChoiceSupport. */
38
39
  toolChoiceSupport?: OpenAICompat["toolChoiceSupport"];
39
40
  };
@@ -146,6 +146,7 @@ export type ResolvedOpenAICompat = Required<
146
146
  | "toolChoiceSupport"
147
147
  | "supportsResponsesSessionAffinity"
148
148
  | "supportsServiceTier"
149
+ | "supportsUltrafastTier"
149
150
  | "reservedToolNames"
150
151
  >
151
152
  > & {
@@ -155,6 +156,7 @@ export type ResolvedOpenAICompat = Required<
155
156
  toolStrictMode: ResolvedToolStrictMode;
156
157
  supportsResponsesSessionAffinity?: OpenAICompat["supportsResponsesSessionAffinity"];
157
158
  supportsServiceTier?: OpenAICompat["supportsServiceTier"];
159
+ supportsUltrafastTier?: OpenAICompat["supportsUltrafastTier"];
158
160
  /** Optional explicit capability override; resolved via deriveToolChoiceSupport. */
159
161
  toolChoiceSupport?: OpenAICompat["toolChoiceSupport"];
160
162
  };
@@ -349,6 +351,8 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
349
351
  supportsDeveloperRole: !isNonStandard,
350
352
  sendSessionHeaders: false,
351
353
  supportsResponsesSessionAffinity: false,
354
+ supportsServiceTier: false,
355
+ supportsUltrafastTier: false,
352
356
  supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
353
357
  supportsReasoningEffort:
354
358
  hasAuditedReasoningEffortTransport &&
@@ -422,6 +426,7 @@ export function resolveOpenAICompat(
422
426
  ? model.compat.supportsResponsesSessionAffinity
423
427
  : undefined) ?? detected.supportsResponsesSessionAffinity,
424
428
  supportsServiceTier: model.compat.supportsServiceTier ?? detected.supportsServiceTier,
429
+ supportsUltrafastTier: model.compat.supportsUltrafastTier ?? detected.supportsUltrafastTier,
425
430
  supportsMultipleSystemMessages:
426
431
  model.compat.supportsMultipleSystemMessages ?? detected.supportsMultipleSystemMessages,
427
432
  supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
@@ -1,5 +1,5 @@
1
1
  import type { ModelManagerOptions } from "../model-manager";
2
- import { fetchAntigravityDiscoveryModels } from "../utils/discovery/antigravity";
2
+ import { fetchAntigravityDiscoveryModels, getDefaultAntigravityUserAgent } from "../utils/discovery/antigravity";
3
3
  import { fetchGeminiModels } from "../utils/discovery/gemini";
4
4
 
5
5
  export interface GoogleModelManagerConfig {
@@ -48,14 +48,17 @@ export function googleAntigravityModelManagerOptions(
48
48
  config?: GoogleAntigravityModelManagerConfig,
49
49
  ): ModelManagerOptions<"google-gemini-cli"> {
50
50
  const token = config?.oauthToken;
51
+ const ua = getDefaultAntigravityUserAgent();
51
52
  return {
52
53
  providerId: "google-antigravity",
54
+ discoveryUserAgent: ua,
53
55
  ...(token
54
56
  ? {
55
57
  fetchDynamicModels: () =>
56
58
  fetchAntigravityDiscoveryModels({
57
59
  token,
58
60
  endpoint: config?.endpoint,
61
+ userAgent: ua,
59
62
  }),
60
63
  }
61
64
  : undefined),
@@ -67,8 +70,10 @@ export function googleGeminiCliModelManagerOptions(
67
70
  ): ModelManagerOptions<"google-gemini-cli"> {
68
71
  const token = config?.oauthToken;
69
72
  const endpoint = config?.endpoint ?? CLOUD_CODE_ASSIST_ENDPOINT;
73
+ const ua = getDefaultAntigravityUserAgent();
70
74
  return {
71
75
  providerId: "google-gemini-cli",
76
+ discoveryUserAgent: ua,
72
77
  ...(token
73
78
  ? {
74
79
  fetchDynamicModels: async () => {
@@ -76,6 +81,7 @@ export function googleGeminiCliModelManagerOptions(
76
81
  token,
77
82
  endpoint,
78
83
  targetProvider: "google-gemini-cli",
84
+ userAgent: ua,
79
85
  });
80
86
  if (models === null) {
81
87
  return null;
@@ -1316,6 +1316,31 @@ export function kiloModelManagerOptions(config?: KiloModelManagerConfig): ModelM
1316
1316
  provider: "kilo",
1317
1317
  baseUrl,
1318
1318
  apiKey,
1319
+ // Kilo serves the OpenRouter catalog shape: nested limits/modalities and
1320
+ // per-token USD string prices (GJC costs are per million tokens).
1321
+ mapModel: (entry, defaults) => {
1322
+ const topProvider = isRecord(entry.top_provider) ? entry.top_provider : undefined;
1323
+ const architecture = isRecord(entry.architecture) ? entry.architecture : undefined;
1324
+ const pricing = isRecord(entry.pricing) ? entry.pricing : undefined;
1325
+ const params = Array.isArray(entry.supported_parameters) ? entry.supported_parameters : [];
1326
+ const perMillion = (value: unknown): number => {
1327
+ const perToken = toNumber(value);
1328
+ return perToken !== undefined && perToken >= 0 ? perToken * 1_000_000 : 0;
1329
+ };
1330
+ return {
1331
+ ...defaults,
1332
+ reasoning: params.includes("reasoning"),
1333
+ input: toInputCapabilities(architecture?.input_modalities),
1334
+ cost: {
1335
+ input: perMillion(pricing?.prompt),
1336
+ output: perMillion(pricing?.completion),
1337
+ cacheRead: perMillion(pricing?.input_cache_read),
1338
+ cacheWrite: perMillion(pricing?.input_cache_write),
1339
+ },
1340
+ contextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),
1341
+ maxTokens: toPositiveNumber(topProvider?.max_completion_tokens, defaults.maxTokens),
1342
+ };
1343
+ },
1319
1344
  }),
1320
1345
  };
1321
1346
  }
@@ -2125,6 +2150,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
2125
2150
  ...(api === "openai-completions"
2126
2151
  ? {
2127
2152
  compat: {
2153
+ ...(reference.compat ?? {}),
2128
2154
  supportsStore: false,
2129
2155
  supportsDeveloperRole: false,
2130
2156
  supportsReasoningEffort: false,
@@ -52,6 +52,7 @@ import { decodeEventStream } from "./aws-eventstream";
52
52
  import type { AwsCredentials } from "./aws-sigv4";
53
53
  import { signRequest } from "./aws-sigv4";
54
54
  import { transformMessages } from "./transform-messages";
55
+ import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
55
56
 
56
57
  export type BedrockThinkingDisplay = "summarized" | "omitted";
57
58
 
@@ -673,12 +674,24 @@ function buildSystemPrompt(
673
674
  return blocks;
674
675
  }
675
676
 
677
+ function convertToolResultContent(
678
+ content: ToolResultMessage["content"],
679
+ supportsImages: boolean,
680
+ ): Array<TextBlockWire | ImageBlockWire> {
681
+ return content.map(c => {
682
+ if (c.type !== "image") return { text: c.text.toWellFormed() };
683
+ if (!supportsImages) return { text: NON_VISION_IMAGE_PLACEHOLDER };
684
+ return { image: createImageBlock(c.mimeType, c.data) };
685
+ });
686
+ }
687
+
676
688
  function convertMessages(
677
689
  context: Context,
678
690
  model: Model<"bedrock-converse-stream">,
679
691
  cacheRetention: CacheRetention,
680
692
  ): WireMessage[] {
681
693
  const result: WireMessage[] = [];
694
+ const supportsImages = model.input.includes("image");
682
695
  const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
683
696
 
684
697
  for (let i = 0; i < transformedMessages.length; i++) {
@@ -702,7 +715,11 @@ function convertMessages(
702
715
  break;
703
716
  }
704
717
  case "image":
705
- contentBlocks.push({ image: createImageBlock(c.mimeType, c.data) });
718
+ contentBlocks.push(
719
+ supportsImages
720
+ ? { image: createImageBlock(c.mimeType, c.data) }
721
+ : { text: NON_VISION_IMAGE_PLACEHOLDER },
722
+ );
706
723
  break;
707
724
  default:
708
725
  throw new Error("Unknown user content type");
@@ -772,11 +789,7 @@ function convertMessages(
772
789
  toolResults.push({
773
790
  toolResult: {
774
791
  toolUseId: normalizeToolCallId(m.toolCallId),
775
- content: m.content.map(c =>
776
- c.type === "image"
777
- ? { image: createImageBlock(c.mimeType, c.data) }
778
- : { text: c.text.toWellFormed() },
779
- ),
792
+ content: convertToolResultContent(m.content, supportsImages),
780
793
  status: m.isError ? "error" : "success",
781
794
  },
782
795
  });
@@ -787,11 +800,7 @@ function convertMessages(
787
800
  toolResults.push({
788
801
  toolResult: {
789
802
  toolUseId: normalizeToolCallId(nextMsg.toolCallId),
790
- content: nextMsg.content.map(c =>
791
- c.type === "image"
792
- ? { image: createImageBlock(c.mimeType, c.data) }
793
- : { text: c.text.toWellFormed() },
794
- ),
803
+ content: convertToolResultContent(nextMsg.content, supportsImages),
795
804
  status: nextMsg.isError ? "error" : "success",
796
805
  },
797
806
  });
@@ -51,6 +51,16 @@ export declare function isAnthropicThinkingBlockMutationError(error: unknown): b
51
51
  * than only the latest one.
52
52
  */
53
53
  export declare function isAnthropicThinkingSignatureInvalidError(error: unknown): boolean;
54
+ /**
55
+ * 400 shape where Anthropic cannot decode one of the request's images:
56
+ * `{"type":"invalid_request_error","message":"Could not process image"}`.
57
+ * Observed for a truncated JPEG (no end-of-image marker) that local resizing
58
+ * also failed to decode and therefore forwarded unchanged. The body names no
59
+ * image, and the image stays in history, so without omission every later
60
+ * request of the session draws the same rejection.
61
+ */
62
+ export declare function isAnthropicImageUnprocessableError(error: unknown): boolean;
63
+ export declare const ANTHROPIC_OMITTED_IMAGE_PLACEHOLDER = "[Image omitted: Anthropic could not process this image. The file may be corrupt or truncated; re-export or convert it before attaching it again.]";
54
64
  /**
55
65
  * CLIProxyAPI replaces Anthropic's rejection body wholesale instead of forwarding
56
66
  * it: the client only ever sees
@@ -388,6 +388,14 @@ type AnthropicProviderSessionState = ProviderSessionState & {
388
388
  thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope;
389
389
  thinkingReplayRepairAttempts: number;
390
390
  thinkingReplayRejectedPayload?: AnthropicPayloadFingerprint;
391
+ /**
392
+ * sha256 digests of base64 image payloads Anthropic rejected with
393
+ * "Could not process image". The image stays in session history, so every
394
+ * later build must keep replacing it or each turn draws the same 400.
395
+ */
396
+ omittedImageDigests: Set<string>;
397
+ /** Image omissions spent since the last completed stream; bounds escalation. */
398
+ imageOmissionRepairs: number;
391
399
  /**
392
400
  * Managed-mode escalation for the CPA alias-restore failure (issue #4338):
393
401
  * corrective steering recorded against one exact turn, applied by the next
@@ -416,6 +424,8 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
416
424
  warnedUnknownLongCacheRetention: new Set(),
417
425
  thinkingReplayRepairScope: "none",
418
426
  thinkingReplayRepairAttempts: 0,
427
+ omittedImageDigests: new Set(),
428
+ imageOmissionRepairs: 0,
419
429
  close: () => {
420
430
  state.strictToolsDisabled = false;
421
431
  state.fastModeDisabled = false;
@@ -424,6 +434,8 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
424
434
  state.thinkingReplayRepairScope = "none";
425
435
  state.thinkingReplayRepairAttempts = 0;
426
436
  state.thinkingReplayRejectedPayload = undefined;
437
+ state.omittedImageDigests.clear();
438
+ state.imageOmissionRepairs = 0;
427
439
  state.cpaToolAliasSteering = undefined;
428
440
  },
429
441
  };
@@ -532,6 +544,86 @@ export function isAnthropicThinkingSignatureInvalidError(error: unknown): boolea
532
544
  );
533
545
  }
534
546
 
547
+ /**
548
+ * 400 shape where Anthropic cannot decode one of the request's images:
549
+ * `{"type":"invalid_request_error","message":"Could not process image"}`.
550
+ * Observed for a truncated JPEG (no end-of-image marker) that local resizing
551
+ * also failed to decode and therefore forwarded unchanged. The body names no
552
+ * image, and the image stays in history, so without omission every later
553
+ * request of the session draws the same rejection.
554
+ */
555
+ export function isAnthropicImageUnprocessableError(error: unknown): boolean {
556
+ if (!isAnthropicInvalidRequestStatus(error)) return false;
557
+ const message = error instanceof Error ? error.message : String(error);
558
+ return /invalid_request_error/i.test(message) && /could not process image/i.test(message);
559
+ }
560
+
561
+ /**
562
+ * Bounds image omission per stream: the first rejection omits the images of the
563
+ * newest message that carries any (the request before it was accepted, so the
564
+ * rejected image is almost always new), the second omits every remaining image.
565
+ */
566
+ const ANTHROPIC_MAX_IMAGE_OMISSION_REPAIRS = 2;
567
+
568
+ export const ANTHROPIC_OMITTED_IMAGE_PLACEHOLDER =
569
+ "[Image omitted: Anthropic could not process this image. The file may be corrupt or truncated; re-export or convert it before attaching it again.]";
570
+
571
+ type AnthropicImageSite = {
572
+ digest: string;
573
+ messageIndex: number;
574
+ container: unknown[];
575
+ index: number;
576
+ };
577
+
578
+ function collectAnthropicImageSites(params: MessageCreateParamsStreaming): AnthropicImageSite[] {
579
+ const sites: AnthropicImageSite[] = [];
580
+ const visit = (container: unknown[], messageIndex: number): void => {
581
+ for (let index = 0; index < container.length; index++) {
582
+ const block = container[index];
583
+ if (!isRecord(block)) continue;
584
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
585
+ visit(block.content, messageIndex);
586
+ continue;
587
+ }
588
+ if (block.type !== "image" || !isRecord(block.source)) continue;
589
+ const data = block.source.data;
590
+ if (typeof data !== "string") continue;
591
+ const digest = nodeCrypto.createHash("sha256").update(data).digest("hex");
592
+ sites.push({ digest, messageIndex, container, index });
593
+ }
594
+ };
595
+ params.messages.forEach((message, messageIndex) => {
596
+ if (Array.isArray(message.content)) visit(message.content as unknown[], messageIndex);
597
+ });
598
+ return sites;
599
+ }
600
+
601
+ /** Replace every image whose payload digest is listed, keeping its cache breakpoint. */
602
+ function omitAnthropicImages(params: MessageCreateParamsStreaming, digests: ReadonlySet<string>): void {
603
+ for (const site of collectAnthropicImageSites(params)) {
604
+ if (!digests.has(site.digest)) continue;
605
+ const image = site.container[site.index] as Record<string, unknown>;
606
+ site.container[site.index] = {
607
+ type: "text",
608
+ text: ANTHROPIC_OMITTED_IMAGE_PLACEHOLDER,
609
+ ...(image.cache_control === undefined ? {} : { cache_control: image.cache_control }),
610
+ };
611
+ }
612
+ }
613
+
614
+ /** Digests to omit after a rejection: the newest image-bearing message first, then everything. */
615
+ function selectAnthropicImagesToOmit(
616
+ params: MessageCreateParamsStreaming,
617
+ omitted: ReadonlySet<string>,
618
+ scope: "latest" | "all",
619
+ ): string[] {
620
+ const remaining = collectAnthropicImageSites(params).filter(site => !omitted.has(site.digest));
621
+ if (remaining.length === 0) return [];
622
+ const newest = Math.max(...remaining.map(site => site.messageIndex));
623
+ const selected = scope === "latest" ? remaining.filter(site => site.messageIndex === newest) : remaining;
624
+ return [...new Set(selected.map(site => site.digest))];
625
+ }
626
+
535
627
  /**
536
628
  * CLIProxyAPI replaces Anthropic's rejection body wholesale instead of forwarding
537
629
  * it: the client only ever sees
@@ -1640,6 +1732,7 @@ function getAnthropicCompat(
1640
1732
  supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
1641
1733
  supportsToolChoice: model.compat?.supportsToolChoice ?? true,
1642
1734
  supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
1735
+ supportsUltrafastTier: model.compat?.supportsUltrafastTier ?? false,
1643
1736
  promptCacheMode: model.compat?.promptCacheMode ?? "none",
1644
1737
  toolChoiceSupport: model.compat?.toolChoiceSupport,
1645
1738
  };
@@ -2095,6 +2188,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2095
2188
  let activeAbortTracker = createAbortSourceTracker(options?.signal);
2096
2189
 
2097
2190
  try {
2191
+ const idleTimeoutMs =
2192
+ options?.streamIdleTimeoutMs ??
2193
+ getStreamIdleTimeoutMs(model.streamIdleTimeoutMs ?? getProviderStreamIdleTimeoutFallbackMs(model.provider));
2098
2194
  let client: Anthropic;
2099
2195
  let isOAuthToken: boolean;
2100
2196
 
@@ -2127,7 +2223,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2127
2223
  requestMaxRetries: options?.requestMaxRetries,
2128
2224
  maxRetryDelayMs: options?.maxRetryDelayMs,
2129
2225
  streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
2130
- streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
2226
+ streamIdleTimeoutMs: idleTimeoutMs,
2131
2227
  providerSessionId: options?.providerSessionId,
2132
2228
  });
2133
2229
  client = created.client;
@@ -2154,6 +2250,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2154
2250
  let thinkingReplayRepairPersistent = thinkingReplayRepairScope !== "none";
2155
2251
  let generatedCacheBudget: GeneratedCacheBudget = providerSessionState?.generatedCacheBudget ?? 2;
2156
2252
  let warnedUnknownLongCacheRetention = false;
2253
+ // Shared with the session so later turns keep omitting a rejected image
2254
+ // that is still in history; per-stream when no session state is shared.
2255
+ const omittedImageDigests = providerSessionState?.omittedImageDigests ?? new Set<string>();
2256
+ let imageOmissionRepairs = providerSessionState?.imageOmissionRepairs ?? 0;
2157
2257
  const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
2158
2258
  // Degradation state is cumulative: every fallback rebuild must merge all
2159
2259
  // repairs activated so far. Rebuilding from only the immediate call lets
@@ -2196,6 +2296,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2196
2296
  if (dropFastMode) {
2197
2297
  dropAnthropicFastMode(nextParams);
2198
2298
  }
2299
+ if (omittedImageDigests.size > 0) {
2300
+ omitAnthropicImages(nextParams, omittedImageDigests);
2301
+ }
2199
2302
  const replacementPayload = await options?.onPayload?.(
2200
2303
  nextParams,
2201
2304
  model,
@@ -2315,9 +2418,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2315
2418
  firstTokenTime = undefined;
2316
2419
  truncatedToolCalls.clear();
2317
2420
  };
2318
- const idleTimeoutMs =
2319
- options?.streamIdleTimeoutMs ??
2320
- getStreamIdleTimeoutMs(getProviderStreamIdleTimeoutFallbackMs(model.provider));
2321
2421
  const firstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(model.provider);
2322
2422
  const firstEventTimeoutMs =
2323
2423
  options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
@@ -2786,6 +2886,11 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2786
2886
  providerSessionState.thinkingReplayRepairScope = "none";
2787
2887
  providerSessionState.thinkingReplayRepairAttempts = 0;
2788
2888
  }
2889
+ // Omitted digests stay recorded (the images are still in history); only
2890
+ // the escalation budget re-arms for a different image rejected later.
2891
+ if (providerSessionState && imageOmissionRepairs > 0) {
2892
+ providerSessionState.imageOmissionRepairs = 0;
2893
+ }
2789
2894
  // Release a recorded CPA steering once any stream completes: a
2790
2895
  // successful stream consumed it, and a failed one ends the turn (a
2791
2896
  // later turn's different user prompt would expire it anyway).
@@ -2889,6 +2994,36 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2889
2994
  resetOutputForRetry();
2890
2995
  continue;
2891
2996
  }
2997
+ // An image Anthropic cannot decode fails the whole request and stays in
2998
+ // history, so the session never recovers on its own. Record the omission
2999
+ // before deciding who retries: unmanaged streams resend here, managed
3000
+ // attempts leave the retry to the fallback controller, whose next attempt
3001
+ // rebuilds from the shared session state without the rejected image.
3002
+ if (
3003
+ firstTokenTime === undefined &&
3004
+ imageOmissionRepairs < ANTHROPIC_MAX_IMAGE_OMISSION_REPAIRS &&
3005
+ isAnthropicImageUnprocessableError(streamFailure)
3006
+ ) {
3007
+ const scope = imageOmissionRepairs === 0 ? "latest" : "all";
3008
+ const digests = selectAnthropicImagesToOmit(params, omittedImageDigests, scope);
3009
+ if (digests.length > 0) {
3010
+ for (const digest of digests) omittedImageDigests.add(digest);
3011
+ imageOmissionRepairs++;
3012
+ if (providerSessionState) providerSessionState.imageOmissionRepairs = imageOmissionRepairs;
3013
+ logger.warn("anthropic: provider could not process an image; omitting it from the request", {
3014
+ model: model.id,
3015
+ scope,
3016
+ omittedImages: digests.length,
3017
+ managed: options?.fallbackManaged === true,
3018
+ });
3019
+ if (!options?.fallbackManaged && !options?.disableProviderRetries) {
3020
+ params = await prepareParams();
3021
+ providerUploadCount++;
3022
+ resetOutputForRetry();
3023
+ continue;
3024
+ }
3025
+ }
3026
+ }
2892
3027
  const thinkingSignatureInvalid = isAnthropicThinkingSignatureInvalidError(streamFailure);
2893
3028
  const thinkingBlocksImmutable = isAnthropicThinkingBlockMutationError(streamFailure);
2894
3029
  const maskedProxyRejection = isAnthropicMaskedProxyRejection(streamFailure);
@@ -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.