@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 +12 -0
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/google-auth.d.ts +4 -2
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/package.json +3 -3
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +46 -15
- package/src/providers/google-auth.d.ts +4 -2
- package/src/providers/google-auth.ts +271 -7
- package/src/providers/google-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +1 -1
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.
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
45
|
-
"@gajae-code/utils": "0.17.
|
|
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.
|
|
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
|
-
): {
|
|
804
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 =
|