@gajae-code/ai 0.17.6 → 0.17.7

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.17.7] - 2026-09-25
6
+
7
+ ### Changed
8
+
9
+ - Bump the spoofed Claude Code version from 2.1.280 to 2.1.281 and Gemini CLI version from 0.60.0 to 0.61.0. Anthropic gates newer models behind a minimum client version, so a stale `claude-cli` fingerprint can return HTTP 400 for an otherwise reachable model; the Gemini CLI fingerprint drifts the same way.
10
+
11
+ ### Fixed
12
+
13
+ - Anthropic-compatible endpoints that leave `compat.supportsLongCacheRetention` unset no longer downgrade the default `long` prompt-cache retention to the ~5m TTL silently: GJC now logs one warning per provider session naming the provider and model and pointing at the flag. Set it to `true` for gateways that forward `ttl: "1h"`, or to `false` to accept the ~5m lifetime and silence the warning. (#5944)
14
+
15
+ - Vertex AI Application Default Credentials now accept `type: "external_account"` (Workload Identity Federation): the subject token is read from `credential_source` (file, url, or opt-in executable), exchanged at STS, and optionally impersonated via `service_account_impersonation_url`, so gjc authenticates from GitHub Actions / GitLab CI without a long-lived service-account key. Unrecognised credential types now fail with a clear "Unsupported Google credential type" error instead of a misleading OAuth client error. (#5929)
16
+
5
17
  ## [0.17.6] - 2026-09-24
6
18
 
7
19
  ## [0.17.5] - 2026-09-24
@@ -107,7 +107,7 @@ export interface CpaToolAliasRestoreFailure {
107
107
  */
108
108
  export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
109
109
  export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
110
- export declare const claudeCodeVersion = "2.1.280";
110
+ export declare const claudeCodeVersion = "2.1.281";
111
111
  export declare const claudeCodeEntrypoint = "sdk-cli";
112
112
  export declare const claudeToolPrefix: string;
113
113
  export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * Replaces `google-auth-library` with a direct WebCrypto + REST implementation.
5
5
  * Sources, in priority order:
6
- * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
7
- * or `type: "authorized_user"` (refresh-token exchange).
6
+ * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange),
7
+ * `type: "authorized_user"` (refresh-token exchange), or `type: "external_account"` (Workload Identity
8
+ * Federation: subject token from `credential_source` → STS token exchange → optional service-account
9
+ * impersonation).
8
10
  * 2. `~/.config/gcloud/application_default_credentials.json` (user ADC, same authorized_user flow).
9
11
  * 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
10
12
  *
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export declare const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
7
7
  export declare const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
- export declare const DEFAULT_GEMINI_CLI_VERSION = "0.60.0";
8
+ export declare const DEFAULT_GEMINI_CLI_VERSION = "0.61.0";
9
9
  export declare function getGeminiCliUserAgent(modelId?: string): string;
10
10
  export declare const getGeminiCliHeaders: (modelId?: string) => {
11
11
  "User-Agent": string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.17.6",
4
+ "version": "0.17.7",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -41,8 +41,8 @@
41
41
  "@agentclientprotocol/sdk": "1.3.0",
42
42
  "@anthropic-ai/sdk": "^0.94.0",
43
43
  "@bufbuild/protobuf": "^2.12.0",
44
- "@gajae-code/natives": "0.17.6",
45
- "@gajae-code/utils": "0.17.6",
44
+ "@gajae-code/natives": "0.17.7",
45
+ "@gajae-code/utils": "0.17.7",
46
46
  "openai": "^6.36.0",
47
47
  "partial-json": "^0.1.7",
48
48
  "zod": "4.4.3"
@@ -107,7 +107,7 @@ export interface CpaToolAliasRestoreFailure {
107
107
  */
108
108
  export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
109
109
  export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
110
- export declare const claudeCodeVersion = "2.1.280";
110
+ export declare const claudeCodeVersion = "2.1.281";
111
111
  export declare const claudeCodeEntrypoint = "sdk-cli";
112
112
  export declare const claudeToolPrefix: string;
113
113
  export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -371,6 +371,8 @@ type AnthropicProviderSessionState = ProviderSessionState & {
371
371
  strictToolsDisabled: boolean;
372
372
  fastModeDisabled: boolean;
373
373
  generatedCacheBudget: GeneratedCacheBudget;
374
+ /** `provider\0model` keys already warned about a long cache-retention downgrade. */
375
+ warnedUnknownLongCacheRetention: Set<string>;
374
376
  thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope;
375
377
  thinkingReplayRepairAttempts: number;
376
378
  thinkingReplayRejectedPayload?: AnthropicPayloadFingerprint;
@@ -399,12 +401,14 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
399
401
  strictToolsDisabled: false,
400
402
  fastModeDisabled: false,
401
403
  generatedCacheBudget: 2,
404
+ warnedUnknownLongCacheRetention: new Set(),
402
405
  thinkingReplayRepairScope: "none",
403
406
  thinkingReplayRepairAttempts: 0,
404
407
  close: () => {
405
408
  state.strictToolsDisabled = false;
406
409
  state.fastModeDisabled = false;
407
410
  state.generatedCacheBudget = 2;
411
+ state.warnedUnknownLongCacheRetention.clear();
408
412
  state.thinkingReplayRepairScope = "none";
409
413
  state.thinkingReplayRepairAttempts = 0;
410
414
  state.thinkingReplayRejectedPayload = undefined;
@@ -800,10 +804,14 @@ function getCacheControl(
800
804
  baseUrl: string,
801
805
  cacheRetention?: CacheRetention,
802
806
  generatedCacheBudget: GeneratedCacheBudget = 2,
803
- ): { mode: AnthropicCacheMode; cacheControl?: AnthropicCacheControl } {
804
- if (generatedCacheBudget === 0) return { mode: "none" };
807
+ ): {
808
+ mode: AnthropicCacheMode;
809
+ cacheControl?: AnthropicCacheControl;
810
+ warnUnknownLongCacheRetention: boolean;
811
+ } {
812
+ if (generatedCacheBudget === 0) return { mode: "none", warnUnknownLongCacheRetention: false };
805
813
  const retention = resolveCacheRetention(cacheRetention ?? model.cacheRetention, "long");
806
- if (retention === "none") return { mode: "none" };
814
+ if (retention === "none") return { mode: "none", warnUnknownLongCacheRetention: false };
807
815
 
808
816
  const isCanonicalApi = isAnthropicApiBaseUrl(baseUrl);
809
817
  const promptCacheMode = model.compat?.promptCacheMode;
@@ -819,13 +827,16 @@ function getCacheControl(
819
827
  : isClaudeFamilyModel(model)
820
828
  ? "explicit"
821
829
  : "none";
822
- if (mode === "none") return { mode };
830
+ if (mode === "none") return { mode, warnUnknownLongCacheRetention: false };
831
+ const warnUnknownLongCacheRetention =
832
+ !isCanonicalApi && retention === "long" && model.compat?.supportsLongCacheRetention === undefined;
823
833
 
824
834
  const supportsLongCacheRetention = isCanonicalApi
825
835
  ? getAnthropicCompat(model).supportsLongCacheRetention
826
836
  : model.compat?.supportsLongCacheRetention === true;
827
837
  return {
828
838
  mode,
839
+ warnUnknownLongCacheRetention,
829
840
  cacheControl: {
830
841
  type: "ephemeral",
831
842
  ...(retention === "long" && supportsLongCacheRetention ? { ttl: "1h" } : {}),
@@ -834,7 +845,7 @@ function getCacheControl(
834
845
  }
835
846
 
836
847
  // Stealth mode: Mimic Anthropic Code headers and tool prefixing.
837
- export const claudeCodeVersion = "2.1.280";
848
+ export const claudeCodeVersion = "2.1.281";
838
849
  export const claudeCodeEntrypoint = "sdk-cli";
839
850
  export const claudeToolPrefix: string = "proxy_";
840
851
  export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -1972,13 +1983,14 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1972
1983
  // never persisted — so a completed stream must not release it.
1973
1984
  let thinkingReplayRepairPersistent = thinkingReplayRepairScope !== "none";
1974
1985
  let generatedCacheBudget: GeneratedCacheBudget = providerSessionState?.generatedCacheBudget ?? 2;
1986
+ let warnedUnknownLongCacheRetention = false;
1975
1987
  const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
1976
1988
  // Degradation state is cumulative: every fallback rebuild must merge all
1977
1989
  // repairs activated so far. Rebuilding from only the immediate call lets
1978
1990
  // a later strict/forced-tool/fast-mode fallback reintroduce the rejected
1979
1991
  // shape (e.g. invalid thinking signatures or forced tool_choice), and
1980
1992
  // the one-shot thinking-repair guard then blocks recovery.
1981
- let nextParams = buildParams(
1993
+ const builtParams = buildParams(
1982
1994
  model,
1983
1995
  baseUrl,
1984
1996
  context,
@@ -1991,6 +2003,20 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1991
2003
  },
1992
2004
  generatedCacheBudget,
1993
2005
  );
2006
+ let nextParams = builtParams.params;
2007
+ const longCacheWarningKey = `${model.provider}\0${model.id}`;
2008
+ if (
2009
+ builtParams.warnUnknownLongCacheRetention &&
2010
+ !warnedUnknownLongCacheRetention &&
2011
+ !providerSessionState?.warnedUnknownLongCacheRetention.has(longCacheWarningKey)
2012
+ ) {
2013
+ warnedUnknownLongCacheRetention = true;
2014
+ providerSessionState?.warnedUnknownLongCacheRetention.add(longCacheWarningKey);
2015
+ logger.warn(
2016
+ 'Anthropic-compatible endpoint has unknown long cache-retention support; omitting ttl (default ~5m). Set compat.supportsLongCacheRetention: true to opt into ttl: "1h".',
2017
+ { provider: model.provider, model: model.id },
2018
+ );
2019
+ }
1994
2020
  if (droppedForcedToolChoice) {
1995
2021
  delete nextParams.tool_choice;
1996
2022
  }
@@ -3554,13 +3580,12 @@ function buildParams(
3554
3580
  disableStrictTools = false,
3555
3581
  thinkingRepair?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
3556
3582
  generatedCacheBudget: GeneratedCacheBudget = 2,
3557
- ): MessageCreateParamsStreaming {
3558
- const { mode: cacheMode, cacheControl } = getCacheControl(
3559
- model,
3560
- baseUrl,
3561
- options?.cacheRetention,
3562
- generatedCacheBudget,
3563
- );
3583
+ ): { params: MessageCreateParamsStreaming; warnUnknownLongCacheRetention: boolean } {
3584
+ const {
3585
+ mode: cacheMode,
3586
+ cacheControl,
3587
+ warnUnknownLongCacheRetention,
3588
+ } = getCacheControl(model, baseUrl, options?.cacheRetention, generatedCacheBudget);
3564
3589
 
3565
3590
  const params: AnthropicSamplingParams = {
3566
3591
  model: model.id,
@@ -3731,11 +3756,17 @@ function buildParams(
3731
3756
  params.system = systemBlocks;
3732
3757
  }
3733
3758
  ensureMaxTokensForThinking(params, model);
3734
- applyPromptCaching(params as AnthropicCacheParams, cacheMode, cacheControl, generatedCacheBudget);
3759
+ const cacheParams = params as AnthropicCacheParams;
3760
+ const cacheBreakpointCountBefore = countCacheControlBreakpoints(cacheParams);
3761
+ applyPromptCaching(cacheParams, cacheMode, cacheControl, generatedCacheBudget);
3735
3762
  enforceCacheControlLimit(params, 4);
3736
3763
  normalizeCacheControlTtlOrdering(params);
3737
3764
 
3738
- return params;
3765
+ return {
3766
+ params,
3767
+ warnUnknownLongCacheRetention:
3768
+ warnUnknownLongCacheRetention && countCacheControlBreakpoints(cacheParams) > cacheBreakpointCountBefore,
3769
+ };
3739
3770
  }
3740
3771
 
3741
3772
  /**
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * Replaces `google-auth-library` with a direct WebCrypto + REST implementation.
5
5
  * Sources, in priority order:
6
- * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
7
- * or `type: "authorized_user"` (refresh-token exchange).
6
+ * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange),
7
+ * `type: "authorized_user"` (refresh-token exchange), or `type: "external_account"` (Workload Identity
8
+ * Federation: subject token from `credential_source` → STS token exchange → optional service-account
9
+ * impersonation).
8
10
  * 2. `~/.config/gcloud/application_default_credentials.json` (user ADC, same authorized_user flow).
9
11
  * 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
10
12
  *
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * Replaces `google-auth-library` with a direct WebCrypto + REST implementation.
5
5
  * Sources, in priority order:
6
- * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
7
- * or `type: "authorized_user"` (refresh-token exchange).
6
+ * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange),
7
+ * `type: "authorized_user"` (refresh-token exchange), or `type: "external_account"` (Workload Identity
8
+ * Federation: subject token from `credential_source` → STS token exchange → optional service-account
9
+ * impersonation).
8
10
  * 2. `~/.config/gcloud/application_default_credentials.json` (user ADC, same authorized_user flow).
9
11
  * 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
10
12
  *
@@ -18,9 +20,17 @@ import { $credentialEnv, $envpos, getTrustedHomeDir, isEnoent, logger } from "@g
18
20
  import type { FetchImpl } from "../types";
19
21
 
20
22
  const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
23
+ const STS_TOKEN_URL = "https://sts.googleapis.com/v1/token";
21
24
  const METADATA_TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
22
25
  const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
23
26
  const JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
27
+ const TOKEN_EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";
28
+ const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
29
+ const ID_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token";
30
+ const JWT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt";
31
+ const SAML_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:saml2";
32
+ const DEFAULT_IMPERSONATION_LIFETIME_SEC = 3600;
33
+ const DEFAULT_EXECUTABLE_TIMEOUT_MS = 30_000;
24
34
 
25
35
  interface CachedToken {
26
36
  token: string;
@@ -41,7 +51,60 @@ interface AuthorizedUserCredentials {
41
51
  refresh_token: string;
42
52
  }
43
53
 
44
- type AdcFileCredentials = ServiceAccountCredentials | AuthorizedUserCredentials;
54
+ /** `credential_source.format` for file/url sources: plain text (default) or a JSON field. */
55
+ interface SubjectTokenFormat {
56
+ type?: "text" | "json";
57
+ subject_token_field_name?: string;
58
+ }
59
+
60
+ interface FileCredentialSource {
61
+ file: string;
62
+ format?: SubjectTokenFormat;
63
+ }
64
+
65
+ interface UrlCredentialSource {
66
+ url: string;
67
+ headers?: Record<string, string>;
68
+ format?: SubjectTokenFormat;
69
+ }
70
+
71
+ interface ExecutableCredentialSource {
72
+ executable: {
73
+ command: string;
74
+ timeout_millis?: number;
75
+ output_file?: string;
76
+ };
77
+ }
78
+
79
+ type CredentialSource = FileCredentialSource | UrlCredentialSource | ExecutableCredentialSource;
80
+
81
+ /**
82
+ * Workload Identity Federation configuration, as written by `gcloud iam workload-identity-pools
83
+ * create-cred-config` and `google-github-actions/auth`.
84
+ */
85
+ interface ExternalAccountCredentials {
86
+ type: "external_account";
87
+ audience: string;
88
+ subject_token_type: string;
89
+ token_url?: string;
90
+ service_account_impersonation_url?: string;
91
+ service_account_impersonation?: { token_lifetime_seconds?: number };
92
+ credential_source: CredentialSource;
93
+ }
94
+
95
+ type AdcFileCredentials = ServiceAccountCredentials | AuthorizedUserCredentials | ExternalAccountCredentials;
96
+
97
+ /** Response of the executable credential source, per the external-account spec (version 1). */
98
+ interface ExecutableResponse {
99
+ version: number;
100
+ success: boolean;
101
+ token_type?: string;
102
+ id_token?: string;
103
+ saml_response?: string;
104
+ expiration_time?: number;
105
+ code?: string;
106
+ message?: string;
107
+ }
45
108
 
46
109
  interface TokenResponse {
47
110
  access_token: string;
@@ -200,16 +263,217 @@ async function postForToken(
200
263
  return (await response.json()) as TokenResponse;
201
264
  }
202
265
 
266
+ /** Pulls the subject token out of a file/url source body according to its declared format. */
267
+ function extractSubjectToken(raw: string, format: SubjectTokenFormat | undefined, origin: string): string {
268
+ let token: unknown;
269
+ if (format?.type === "json") {
270
+ const field = format.subject_token_field_name;
271
+ if (!field)
272
+ throw new Error(`external_account credential_source ${origin}: json format requires subject_token_field_name`);
273
+ let parsed: unknown;
274
+ try {
275
+ parsed = JSON.parse(raw);
276
+ } catch {
277
+ throw new Error(`external_account credential_source ${origin}: subject token is not valid JSON`);
278
+ }
279
+ token = typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>)[field] : undefined;
280
+ } else {
281
+ token = raw.trim();
282
+ }
283
+ if (typeof token !== "string" || token.length === 0) {
284
+ throw new Error(`external_account credential_source ${origin}: subject token is empty`);
285
+ }
286
+ return token;
287
+ }
288
+
289
+ async function readSubjectTokenFromExecutable(
290
+ creds: ExternalAccountCredentials,
291
+ source: ExecutableCredentialSource,
292
+ ): Promise<string> {
293
+ // Same opt-in the official client libraries require: the credential file names a
294
+ // command to run, so the operator must explicitly allow it.
295
+ if ($credentialEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES") !== "1") {
296
+ throw new Error(
297
+ "external_account executable credential_source requires GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1",
298
+ );
299
+ }
300
+ const { command, timeout_millis, output_file } = source.executable;
301
+ const argv = command.trim().split(/\s+/);
302
+ if (!argv[0]) throw new Error("external_account executable credential_source: command is empty");
303
+ const env: Record<string, string | undefined> = {
304
+ ...process.env,
305
+ GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE: creds.audience,
306
+ GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE: creds.subject_token_type,
307
+ GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE: "0",
308
+ };
309
+ const impersonated = creds.service_account_impersonation_url?.match(/serviceAccounts\/([^/:]+)/)?.[1];
310
+ if (impersonated) env.GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL = impersonated;
311
+ if (output_file) env.GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE = output_file;
312
+
313
+ const proc = Bun.spawn(argv, { env, stdout: "pipe", stderr: "ignore", stdin: "ignore" });
314
+ const timeout = setTimeout(() => proc.kill(), timeout_millis ?? DEFAULT_EXECUTABLE_TIMEOUT_MS);
315
+ let stdout: string;
316
+ try {
317
+ stdout = await new Response(proc.stdout).text();
318
+ const exitCode = await proc.exited;
319
+ if (exitCode !== 0) {
320
+ throw new Error(`external_account executable credential_source exited with code ${exitCode}`);
321
+ }
322
+ } finally {
323
+ clearTimeout(timeout);
324
+ }
325
+ // The spec allows the executable to write its response to output_file instead of stdout.
326
+ const raw = stdout.trim() || (output_file ? await Bun.file(output_file).text() : "");
327
+ let response: ExecutableResponse;
328
+ try {
329
+ response = JSON.parse(raw) as ExecutableResponse;
330
+ } catch {
331
+ throw new Error("external_account executable credential_source returned invalid JSON");
332
+ }
333
+ if (response.version !== 1) {
334
+ throw new Error(`external_account executable response version ${response.version} is not supported`);
335
+ }
336
+ if (!response.success) {
337
+ throw new Error(
338
+ `external_account executable credential_source failed (${response.code ?? "unknown"}): ${response.message ?? ""}`,
339
+ );
340
+ }
341
+ if (response.expiration_time !== undefined && response.expiration_time * 1000 <= Date.now()) {
342
+ throw new Error("external_account executable credential_source returned an expired subject token");
343
+ }
344
+ const token =
345
+ response.token_type === SAML_TOKEN_TYPE
346
+ ? response.saml_response
347
+ : response.token_type === ID_TOKEN_TYPE || response.token_type === JWT_TOKEN_TYPE
348
+ ? response.id_token
349
+ : undefined;
350
+ if (!token) {
351
+ throw new Error(
352
+ `external_account executable credential_source returned unsupported token_type ${response.token_type}`,
353
+ );
354
+ }
355
+ return token;
356
+ }
357
+
358
+ async function readSubjectToken(
359
+ creds: ExternalAccountCredentials,
360
+ signal: AbortSignal | undefined,
361
+ fetchImpl: FetchImpl,
362
+ ): Promise<string> {
363
+ const source = creds.credential_source;
364
+ if (!source || typeof source !== "object") {
365
+ throw new Error("external_account credentials are missing credential_source");
366
+ }
367
+ if ("file" in source) {
368
+ let raw: string;
369
+ try {
370
+ raw = await Bun.file(source.file).text();
371
+ } catch (err) {
372
+ if (isEnoent(err)) throw new Error(`external_account credential_source file is missing: ${source.file}`);
373
+ throw err;
374
+ }
375
+ return extractSubjectToken(raw, source.format, `file ${source.file}`);
376
+ }
377
+ if ("url" in source) {
378
+ const response = await fetchImpl(source.url, { method: "GET", headers: source.headers ?? {}, signal });
379
+ if (!response.ok) {
380
+ throw new Error(`external_account credential_source url returned ${response.status}`);
381
+ }
382
+ return extractSubjectToken(await response.text(), source.format, "url");
383
+ }
384
+ if ("executable" in source) return readSubjectTokenFromExecutable(creds, source);
385
+ throw new Error(
386
+ "external_account credential_source must be one of file, url, or executable (AWS environment_id sources are not supported)",
387
+ );
388
+ }
389
+
390
+ /** STS token exchange (RFC 8693) of the federated subject token for a Google access token. */
391
+ async function exchangeSubjectToken(
392
+ creds: ExternalAccountCredentials,
393
+ subjectToken: string,
394
+ signal: AbortSignal | undefined,
395
+ fetchImpl: FetchImpl,
396
+ ): Promise<TokenResponse> {
397
+ const body = new URLSearchParams({
398
+ grant_type: TOKEN_EXCHANGE_GRANT,
399
+ audience: creds.audience,
400
+ scope: CLOUD_PLATFORM_SCOPE,
401
+ requested_token_type: ACCESS_TOKEN_TYPE,
402
+ subject_token: subjectToken,
403
+ subject_token_type: creds.subject_token_type,
404
+ });
405
+ return postForToken(creds.token_url ?? STS_TOKEN_URL, body, signal, fetchImpl);
406
+ }
407
+
408
+ /** IAM Credentials `generateAccessToken` with the federated token as bearer. */
409
+ async function impersonateServiceAccount(
410
+ creds: ExternalAccountCredentials,
411
+ url: string,
412
+ federatedToken: string,
413
+ signal: AbortSignal | undefined,
414
+ fetchImpl: FetchImpl,
415
+ ): Promise<TokenResponse> {
416
+ const lifetime = creds.service_account_impersonation?.token_lifetime_seconds ?? DEFAULT_IMPERSONATION_LIFETIME_SEC;
417
+ const response = await fetchImpl(url, {
418
+ method: "POST",
419
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${federatedToken}` },
420
+ body: JSON.stringify({ scope: [CLOUD_PLATFORM_SCOPE], lifetime: `${lifetime}s` }),
421
+ signal,
422
+ });
423
+ if (!response.ok) {
424
+ const detail = await response.text().catch(() => "");
425
+ throw new Error(`Google service account impersonation failed (${response.status}): ${detail}`);
426
+ }
427
+ const payload = (await response.json()) as { accessToken?: string; expireTime?: string };
428
+ if (!payload.accessToken) throw new Error("Google service account impersonation returned no accessToken");
429
+ const expiresAtMs = payload.expireTime ? Date.parse(payload.expireTime) : Number.NaN;
430
+ const expiresIn = Number.isFinite(expiresAtMs)
431
+ ? Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000))
432
+ : lifetime;
433
+ return { access_token: payload.accessToken, expires_in: expiresIn };
434
+ }
435
+
436
+ async function exchangeExternalAccount(
437
+ creds: ExternalAccountCredentials,
438
+ signal: AbortSignal | undefined,
439
+ fetchImpl: FetchImpl,
440
+ ): Promise<TokenResponse> {
441
+ if (!creds.audience || !creds.subject_token_type) {
442
+ throw new Error("external_account credentials require audience and subject_token_type");
443
+ }
444
+ const subjectToken = await readSubjectToken(creds, signal, fetchImpl);
445
+ const federated = await exchangeSubjectToken(creds, subjectToken, signal, fetchImpl);
446
+ const impersonationUrl = creds.service_account_impersonation_url;
447
+ if (!impersonationUrl) return federated;
448
+ return impersonateServiceAccount(creds, impersonationUrl, federated.access_token, signal, fetchImpl);
449
+ }
450
+
451
+ async function exchangeAdcCredentials(
452
+ creds: AdcFileCredentials,
453
+ signal: AbortSignal | undefined,
454
+ fetchImpl: FetchImpl,
455
+ ): Promise<TokenResponse> {
456
+ switch (creds.type) {
457
+ case "service_account":
458
+ return exchangeJwtForToken(creds, signal, fetchImpl);
459
+ case "authorized_user":
460
+ return exchangeRefreshToken(creds, signal, fetchImpl);
461
+ case "external_account":
462
+ return exchangeExternalAccount(creds, signal, fetchImpl);
463
+ default:
464
+ throw new Error(
465
+ `Unsupported Google credential type ${JSON.stringify((creds as { type?: unknown }).type)}; expected service_account, authorized_user, or external_account`,
466
+ );
467
+ }
468
+ }
469
+
203
470
  async function resolveAccessTokenUncached(
204
471
  signal: AbortSignal | undefined,
205
472
  fetchImpl: FetchImpl,
206
473
  ): Promise<{ source: string; token: TokenResponse }> {
207
474
  const adc = await loadAdcCredentials();
208
475
  if (adc) {
209
- const token =
210
- adc.creds.type === "service_account"
211
- ? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
212
- : await exchangeRefreshToken(adc.creds, signal, fetchImpl);
476
+ const token = await exchangeAdcCredentials(adc.creds, signal, fetchImpl);
213
477
  return { source: adc.source, token };
214
478
  }
215
479
  const metadata = await fetchMetadataToken(signal, fetchImpl);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export declare const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
7
7
  export declare const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
- export declare const DEFAULT_GEMINI_CLI_VERSION = "0.60.0";
8
+ export declare const DEFAULT_GEMINI_CLI_VERSION = "0.61.0";
9
9
  export declare function getGeminiCliUserAgent(modelId?: string): string;
10
10
  export declare const getGeminiCliHeaders: (modelId?: string) => {
11
11
  "User-Agent": string;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
7
7
  export const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
- export const DEFAULT_GEMINI_CLI_VERSION = "0.60.0";
8
+ export const DEFAULT_GEMINI_CLI_VERSION = "0.61.0";
9
9
 
10
10
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
11
11
  const version =