@gajae-code/ai 0.9.1 → 0.9.3
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/auth-storage.d.ts +16 -0
- package/dist/types/model-retirements.d.ts +6 -0
- package/dist/types/models.d.ts +11 -2
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/utils/discovery/antigravity.d.ts +6 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +134 -12
- package/src/model-cache.ts +1 -1
- package/src/model-manager.ts +14 -3
- package/src/model-retirements.ts +13 -0
- package/src/models.json +0 -29
- package/src/models.ts +22 -5
- package/src/provider-models/google.ts +1 -0
- package/src/providers/anthropic.ts +3 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/stream.ts +85 -36
- package/src/utils/discovery/antigravity.ts +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.2] - 2026-07-09
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added runtime credential selectors so callers can pin stored multi-account credentials by id, email, account id, or project id instead of using automatic rotation/ranking.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Refreshed the default Gemini CLI impersonation version to 0.50.0 so the spoofed User-Agent freshness gate passes for the 0.9.2 release.
|
|
13
|
+
- Hid the non-callable `google-antigravity/gemini-3.1-pro-high` selector from bundled, dynamic, and cached Antigravity catalogs after live Cloud Code Assist calls returned HTTP 400; `google-antigravity/gemini-3.1-pro-low:high` remains the working high-thinking path.
|
|
14
|
+
- Preserved Anthropic tool-use arguments supplied on `content_block_start` when no `input_json_delta` chunks follow, preventing finished tool calls from collapsing back to `{}`.
|
|
15
|
+
- Refreshed the default Gemini CLI impersonation version to 0.50.0 so the spoofed User-Agent freshness gate passes for the 0.9.2 release.
|
|
16
|
+
|
|
5
17
|
## [0.9.1] - 2026-07-08
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -323,7 +323,14 @@ type AuthApiKeyOptions = {
|
|
|
323
323
|
* stranding the caller for `timeoutMs * (maxRetries + 1)`.
|
|
324
324
|
*/
|
|
325
325
|
signal?: AbortSignal;
|
|
326
|
+
/** Pin selection to one stored credential instead of using round-robin/ranking. */
|
|
327
|
+
credentialSelector?: AuthCredentialSelector;
|
|
326
328
|
};
|
|
329
|
+
export type AuthCredentialSelectorKind = "id" | "email" | "account" | "project";
|
|
330
|
+
export interface AuthCredentialSelector {
|
|
331
|
+
kind: AuthCredentialSelectorKind;
|
|
332
|
+
value: string;
|
|
333
|
+
}
|
|
327
334
|
/**
|
|
328
335
|
* Refreshed OAuth access plus identity metadata returned by
|
|
329
336
|
* {@link AuthStorage.getOAuthAccess}. Callers that authenticate via a bearer
|
|
@@ -390,6 +397,15 @@ export declare class AuthStorage {
|
|
|
390
397
|
* Used for CLI --api-key flag.
|
|
391
398
|
*/
|
|
392
399
|
setRuntimeApiKey(provider: string, apiKey: string): void;
|
|
400
|
+
/**
|
|
401
|
+
* Pin credential selection for a provider (not persisted to disk).
|
|
402
|
+
* Used for CLI --credential.
|
|
403
|
+
*/
|
|
404
|
+
setRuntimeCredentialSelector(provider: string, selector: AuthCredentialSelector): void;
|
|
405
|
+
/**
|
|
406
|
+
* Remove a runtime credential selector.
|
|
407
|
+
*/
|
|
408
|
+
removeRuntimeCredentialSelector(provider: string): void;
|
|
393
409
|
/**
|
|
394
410
|
* Remove a runtime API key override.
|
|
395
411
|
*/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high"];
|
|
2
|
+
export declare function isRetiredModelKey(provider: string, modelId: string): boolean;
|
|
3
|
+
export declare function isRetiredModel(model: {
|
|
4
|
+
provider: string;
|
|
5
|
+
id: string;
|
|
6
|
+
}): boolean;
|
package/dist/types/models.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import MODELS from "./models.json";
|
|
2
1
|
import type { Api, KnownProvider, Model, Usage } from "./types";
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Static bundled model registry loaded lazily from `models.json`.
|
|
4
|
+
*
|
|
5
|
+
* This module intentionally exposes compile-time defaults only.
|
|
6
|
+
* It does not include runtime discovery, models.dev overlays, or on-disk cache state.
|
|
7
|
+
*
|
|
8
|
+
* For runtime-aware resolution, use `createModelManager()` / `resolveProviderModels()`.
|
|
9
|
+
*/
|
|
10
|
+
type BundledCatalog = typeof import("./models.json");
|
|
11
|
+
export type GeneratedProvider = keyof BundledCatalog;
|
|
4
12
|
export declare function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi>;
|
|
5
13
|
export declare function getBundledProviders(): KnownProvider[];
|
|
6
14
|
export declare function getBundledModels(provider: GeneratedProvider): Model<Api>[];
|
|
@@ -10,3 +18,4 @@ export declare function calculateCost<TApi extends Api>(model: Model<TApi>, usag
|
|
|
10
18
|
* Returns false if either model is null or undefined.
|
|
11
19
|
*/
|
|
12
20
|
export declare function modelsAreEqual<TApi extends Api>(a: Model<TApi> | null | undefined, b: Model<TApi> | null | undefined): boolean;
|
|
21
|
+
export {};
|
|
@@ -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.50.0";
|
|
9
9
|
export declare function getGeminiCliUserAgent(modelId?: string): string;
|
|
10
10
|
export declare const getGeminiCliHeaders: (modelId?: string) => {
|
|
11
11
|
"User-Agent": string;
|
|
@@ -51,6 +51,12 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
51
51
|
signal?: AbortSignal;
|
|
52
52
|
/** Optional fetch implementation override for tests. */
|
|
53
53
|
fetcher?: typeof fetch;
|
|
54
|
+
/**
|
|
55
|
+
* Provider id the caller assigns to returned models. Scopes retired-selector
|
|
56
|
+
* filtering (e.g. `google-gemini-cli` reuses this helper and remaps rows).
|
|
57
|
+
* Default: `google-antigravity`.
|
|
58
|
+
*/
|
|
59
|
+
targetProvider?: "google-antigravity" | "google-gemini-cli";
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
62
|
* Fetches discoverable Antigravity models and normalizes them into canonical model entries.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.3",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@gajae-code/utils": "0.9.
|
|
43
|
+
"@gajae-code/utils": "0.9.3",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -493,7 +493,16 @@ type AuthApiKeyOptions = {
|
|
|
493
493
|
* stranding the caller for `timeoutMs * (maxRetries + 1)`.
|
|
494
494
|
*/
|
|
495
495
|
signal?: AbortSignal;
|
|
496
|
+
/** Pin selection to one stored credential instead of using round-robin/ranking. */
|
|
497
|
+
credentialSelector?: AuthCredentialSelector;
|
|
496
498
|
};
|
|
499
|
+
export type AuthCredentialSelectorKind = "id" | "email" | "account" | "project";
|
|
500
|
+
|
|
501
|
+
export interface AuthCredentialSelector {
|
|
502
|
+
kind: AuthCredentialSelectorKind;
|
|
503
|
+
value: string;
|
|
504
|
+
}
|
|
505
|
+
|
|
497
506
|
type OAuthResolutionResult = { apiKey: string; credential: OAuthCredential };
|
|
498
507
|
|
|
499
508
|
/**
|
|
@@ -696,6 +705,7 @@ export class AuthStorage {
|
|
|
696
705
|
#data: Map<string, StoredCredential[]> = new Map();
|
|
697
706
|
#runtimeOverrides: Map<string, string> = new Map();
|
|
698
707
|
#configOverrides: Map<string, string> = new Map();
|
|
708
|
+
#runtimeCredentialSelectors: Map<string, AuthCredentialSelector> = new Map();
|
|
699
709
|
/** Tracks next credential index per provider:type key for round-robin distribution (non-session use). */
|
|
700
710
|
#providerRoundRobinIndex: Map<string, number> = new Map();
|
|
701
711
|
/** Tracks the last used credential per provider for a session (used for rate-limit switching). */
|
|
@@ -846,6 +856,23 @@ export class AuthStorage {
|
|
|
846
856
|
this.#runtimeOverrides.set(provider, apiKey);
|
|
847
857
|
}
|
|
848
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Pin credential selection for a provider (not persisted to disk).
|
|
861
|
+
* Used for CLI --credential.
|
|
862
|
+
*/
|
|
863
|
+
setRuntimeCredentialSelector(provider: string, selector: AuthCredentialSelector): void {
|
|
864
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
865
|
+
this.#assertCredentialSelectorUsable(storageProvider, selector);
|
|
866
|
+
this.#runtimeCredentialSelectors.set(storageProvider, selector);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Remove a runtime credential selector.
|
|
871
|
+
*/
|
|
872
|
+
removeRuntimeCredentialSelector(provider: string): void {
|
|
873
|
+
this.#runtimeCredentialSelectors.delete(resolveOAuthStorageProvider(provider));
|
|
874
|
+
}
|
|
875
|
+
|
|
849
876
|
/**
|
|
850
877
|
* Remove a runtime API key override.
|
|
851
878
|
*/
|
|
@@ -1122,6 +1149,72 @@ export class AuthStorage {
|
|
|
1122
1149
|
}
|
|
1123
1150
|
}
|
|
1124
1151
|
|
|
1152
|
+
#formatCredentialSelector(selector: AuthCredentialSelector): string {
|
|
1153
|
+
return `${selector.kind}:${selector.value}`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
#credentialMatchesSelector(entry: StoredCredential, selector: AuthCredentialSelector): boolean {
|
|
1157
|
+
switch (selector.kind) {
|
|
1158
|
+
case "id":
|
|
1159
|
+
return String(entry.id) === selector.value;
|
|
1160
|
+
case "email":
|
|
1161
|
+
return (
|
|
1162
|
+
entry.credential.type === "oauth" &&
|
|
1163
|
+
typeof entry.credential.email === "string" &&
|
|
1164
|
+
entry.credential.email.toLowerCase() === selector.value.toLowerCase()
|
|
1165
|
+
);
|
|
1166
|
+
case "account":
|
|
1167
|
+
return entry.credential.type === "oauth" && entry.credential.accountId === selector.value;
|
|
1168
|
+
case "project":
|
|
1169
|
+
return entry.credential.type === "oauth" && entry.credential.projectId === selector.value;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
#findCredentialBySelector(
|
|
1174
|
+
provider: string,
|
|
1175
|
+
selector: AuthCredentialSelector,
|
|
1176
|
+
): ({ index: number } & StoredCredential) | undefined {
|
|
1177
|
+
const stored = this.#getStoredCredentials(provider);
|
|
1178
|
+
for (let index = 0; index < stored.length; index++) {
|
|
1179
|
+
const entry = stored[index];
|
|
1180
|
+
if (entry && this.#credentialMatchesSelector(entry, selector)) return { ...entry, index };
|
|
1181
|
+
}
|
|
1182
|
+
return undefined;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
#getCredentialSelector(provider: string, options?: AuthApiKeyOptions): AuthCredentialSelector | undefined {
|
|
1186
|
+
return options?.credentialSelector ?? this.#runtimeCredentialSelectors.get(resolveOAuthStorageProvider(provider));
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
#assertCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector): void {
|
|
1190
|
+
if (this.#runtimeOverrides.has(provider)) {
|
|
1191
|
+
throw new Error(
|
|
1192
|
+
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a runtime API key override is active`,
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
if (this.#configOverrides.has(provider)) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a config API key override is active`,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
if (!this.#findCredentialBySelector(provider, selector)) {
|
|
1201
|
+
throw new Error(`No credential found for ${provider} matching ${this.#formatCredentialSelector(selector)}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
#resolveSelectedStoredCredential(
|
|
1206
|
+
provider: string,
|
|
1207
|
+
options?: AuthApiKeyOptions,
|
|
1208
|
+
): ({ index: number } & StoredCredential) | undefined {
|
|
1209
|
+
const selector = this.#getCredentialSelector(provider, options);
|
|
1210
|
+
if (!selector) return undefined;
|
|
1211
|
+
this.#assertCredentialSelectorUsable(resolveOAuthStorageProvider(provider), selector);
|
|
1212
|
+
const selected = this.#findCredentialBySelector(provider, selector);
|
|
1213
|
+
if (!selected) {
|
|
1214
|
+
throw new Error(`No credential found for ${provider} matching ${this.#formatCredentialSelector(selector)}`);
|
|
1215
|
+
}
|
|
1216
|
+
return selected;
|
|
1217
|
+
}
|
|
1125
1218
|
/**
|
|
1126
1219
|
* Selects a credential of the specified type for a provider.
|
|
1127
1220
|
* Returns both the credential and its index in the original array (for updates/removal).
|
|
@@ -2734,17 +2827,28 @@ export class AuthStorage {
|
|
|
2734
2827
|
sessionId?: string,
|
|
2735
2828
|
options?: AuthApiKeyOptions,
|
|
2736
2829
|
): Promise<OAuthResolutionResult | undefined> {
|
|
2737
|
-
const
|
|
2738
|
-
|
|
2739
|
-
|
|
2830
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options);
|
|
2831
|
+
const selectedOAuthCredential =
|
|
2832
|
+
selectedCredential?.credential.type === "oauth"
|
|
2833
|
+
? { credential: selectedCredential.credential, index: selectedCredential.index }
|
|
2834
|
+
: undefined;
|
|
2835
|
+
if (selectedCredential && !selectedOAuthCredential) return undefined;
|
|
2836
|
+
const credentials = selectedOAuthCredential
|
|
2837
|
+
? [selectedOAuthCredential]
|
|
2838
|
+
: this.#getCredentialsForProvider(provider)
|
|
2839
|
+
.map((credential, index) => ({ credential, index }))
|
|
2840
|
+
.filter(
|
|
2841
|
+
(entry): entry is { credential: OAuthCredential; index: number } => entry.credential.type === "oauth",
|
|
2842
|
+
);
|
|
2740
2843
|
|
|
2741
2844
|
if (credentials.length === 0) return undefined;
|
|
2742
2845
|
|
|
2743
2846
|
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
2744
|
-
const order = this.#getCredentialOrder(providerKey, sessionId, credentials.length);
|
|
2847
|
+
const order = selectedCredential ? [0] : this.#getCredentialOrder(providerKey, sessionId, credentials.length);
|
|
2745
2848
|
const strategy = this.#rankingStrategyResolver?.(provider);
|
|
2746
2849
|
const requiresProModel = requiresOpenAICodexProModel(provider, options?.modelId);
|
|
2747
|
-
const checkUsage =
|
|
2850
|
+
const checkUsage =
|
|
2851
|
+
strategy !== undefined && (selectedCredential !== undefined || credentials.length > 1 || requiresProModel);
|
|
2748
2852
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
2749
2853
|
const sessionPreferredIndex = sessionCredential?.type === "oauth" ? sessionCredential.index : undefined;
|
|
2750
2854
|
// Skip ranking only when the session already has a working preferred credential — re-ranking
|
|
@@ -2753,7 +2857,7 @@ export class AuthStorage {
|
|
|
2753
2857
|
// with the most headroom proactively and fall back intelligently when rate-limited.
|
|
2754
2858
|
const sessionPreferredIsAvailable =
|
|
2755
2859
|
sessionPreferredIndex !== undefined && !this.#isCredentialBlocked(providerKey, sessionPreferredIndex);
|
|
2756
|
-
const shouldRank = checkUsage && (!sessionPreferredIsAvailable || requiresProModel);
|
|
2860
|
+
const shouldRank = !selectedCredential && checkUsage && (!sessionPreferredIsAvailable || requiresProModel);
|
|
2757
2861
|
const candidates = shouldRank
|
|
2758
2862
|
? await this.#rankOAuthSelections({ providerKey, provider, order, credentials, options, strategy: strategy! })
|
|
2759
2863
|
: order
|
|
@@ -2761,7 +2865,7 @@ export class AuthStorage {
|
|
|
2761
2865
|
.filter((selection): selection is { credential: OAuthCredential; index: number } => Boolean(selection))
|
|
2762
2866
|
.map(selection => ({ selection, usage: null, usageChecked: false }));
|
|
2763
2867
|
|
|
2764
|
-
if (sessionPreferredIndex !== undefined && !requiresProModel) {
|
|
2868
|
+
if (!selectedCredential && sessionPreferredIndex !== undefined && !requiresProModel) {
|
|
2765
2869
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
2766
2870
|
candidate =>
|
|
2767
2871
|
!this.#isCredentialBlocked(providerKey, candidate.selection.index) &&
|
|
@@ -3118,7 +3222,10 @@ export class AuthStorage {
|
|
|
3118
3222
|
await this.reload();
|
|
3119
3223
|
return this.#resolveOAuthSelection(provider, sessionId, options);
|
|
3120
3224
|
}
|
|
3121
|
-
if (
|
|
3225
|
+
if (
|
|
3226
|
+
!this.#getCredentialSelector(provider, options) &&
|
|
3227
|
+
this.#getCredentialsForProvider(provider).some(credential => credential.type === "oauth")
|
|
3228
|
+
) {
|
|
3122
3229
|
return this.#resolveOAuthSelection(provider, sessionId, options);
|
|
3123
3230
|
}
|
|
3124
3231
|
} else {
|
|
@@ -3126,6 +3233,12 @@ export class AuthStorage {
|
|
|
3126
3233
|
this.#markCredentialBlocked(providerKey, selection.index, Date.now() + 5 * 60 * 1000);
|
|
3127
3234
|
}
|
|
3128
3235
|
}
|
|
3236
|
+
if (this.#getCredentialSelector(provider, options)) {
|
|
3237
|
+
const selector = this.#getCredentialSelector(provider, options);
|
|
3238
|
+
throw new Error(
|
|
3239
|
+
`Selected credential for ${provider} (${selector ? this.#formatCredentialSelector(selector) : "unknown"}) is unavailable`,
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3129
3242
|
|
|
3130
3243
|
return undefined;
|
|
3131
3244
|
}
|
|
@@ -3184,6 +3297,8 @@ export class AuthStorage {
|
|
|
3184
3297
|
* 6. Fallback resolver (models.yml custom providers, last-resort)
|
|
3185
3298
|
*/
|
|
3186
3299
|
async getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
|
|
3300
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options);
|
|
3301
|
+
|
|
3187
3302
|
// Runtime override takes highest priority
|
|
3188
3303
|
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
3189
3304
|
if (runtimeKey) {
|
|
@@ -3200,10 +3315,17 @@ export class AuthStorage {
|
|
|
3200
3315
|
return configKey;
|
|
3201
3316
|
}
|
|
3202
3317
|
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
this.#
|
|
3206
|
-
|
|
3318
|
+
if (selectedCredential?.credential.type === "api_key") {
|
|
3319
|
+
this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
|
|
3320
|
+
return this.#configValueResolver(selectedCredential.credential.key);
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
if (!selectedCredential) {
|
|
3324
|
+
const apiKeySelection = this.#selectCredentialByType(provider, "api_key", sessionId);
|
|
3325
|
+
if (apiKeySelection) {
|
|
3326
|
+
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
3327
|
+
return this.#configValueResolver(apiKeySelection.credential.key);
|
|
3328
|
+
}
|
|
3207
3329
|
}
|
|
3208
3330
|
|
|
3209
3331
|
const oauthResolved = await this.#resolveOAuthSelection(provider, sessionId, options);
|
package/src/model-cache.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Replaces per-provider JSON files with a single cache.db.
|
|
4
4
|
*/
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
|
-
import { getModelDbPath } from "@gajae-code/utils";
|
|
6
|
+
import { getModelDbPath } from "@gajae-code/utils/dirs";
|
|
7
7
|
import type { Api, Model } from "./types";
|
|
8
8
|
|
|
9
9
|
const CACHE_SCHEMA_VERSION = 3;
|
package/src/model-manager.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readModelCache, writeModelCache } from "./model-cache";
|
|
2
|
+
import { isRetiredModel, isRetiredModelKey } from "./model-retirements";
|
|
2
3
|
import { applyGeneratedModelPolicies, enrichModelThinking } from "./model-thinking";
|
|
3
4
|
import { type GeneratedProvider, getBundledModels } from "./models";
|
|
4
5
|
import type { Api, Model, Provider } from "./types";
|
|
5
|
-
import { isRecord } from "./utils";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
8
8
|
const NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
|
|
@@ -85,7 +85,14 @@ function passModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
|
|
|
85
85
|
}
|
|
86
86
|
const out: Model<TApi>[] = [];
|
|
87
87
|
for (const item of value) {
|
|
88
|
-
if (item === null || typeof item !== "object"
|
|
88
|
+
if (item === null || typeof item !== "object") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const candidate = item as { id?: unknown; provider?: unknown };
|
|
92
|
+
if (typeof candidate.id !== "string") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (typeof candidate.provider === "string" && isRetiredModelKey(candidate.provider, candidate.id)) {
|
|
89
96
|
continue;
|
|
90
97
|
}
|
|
91
98
|
out.push(enrichModelThinking(item as Model<TApi>));
|
|
@@ -382,7 +389,7 @@ function normalizeModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
|
|
|
382
389
|
}
|
|
383
390
|
const models: Model<TApi>[] = [];
|
|
384
391
|
for (const item of value) {
|
|
385
|
-
if (isModelLike(item)) {
|
|
392
|
+
if (isModelLike(item) && !isRetiredModel(item)) {
|
|
386
393
|
models.push(enrichModelThinking(item as Model<TApi>));
|
|
387
394
|
}
|
|
388
395
|
}
|
|
@@ -441,6 +448,10 @@ function isModelLike(value: unknown): value is Model<Api> {
|
|
|
441
448
|
return true;
|
|
442
449
|
}
|
|
443
450
|
|
|
451
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
452
|
+
return typeof value === "object" && value !== null;
|
|
453
|
+
}
|
|
454
|
+
|
|
444
455
|
function isModelInputArray(value: unknown): value is ("text" | "image")[] {
|
|
445
456
|
if (!Array.isArray(value) || value.length === 0) {
|
|
446
457
|
return false;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Retired from advertised catalogs because Cloud Code Assist rejects live calls
|
|
2
|
+
// with HTTP 400. The callable high-thinking path is gemini-3.1-pro-low:high.
|
|
3
|
+
export const RETIRED_MODEL_KEYS = ["google-antigravity/gemini-3.1-pro-high"] as const;
|
|
4
|
+
|
|
5
|
+
const RETIRED_MODEL_KEY_SET = new Set<string>(RETIRED_MODEL_KEYS);
|
|
6
|
+
|
|
7
|
+
export function isRetiredModelKey(provider: string, modelId: string): boolean {
|
|
8
|
+
return RETIRED_MODEL_KEY_SET.has(`${provider}/${modelId}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isRetiredModel(model: { provider: string; id: string }): boolean {
|
|
12
|
+
return isRetiredModelKey(model.provider, model.id);
|
|
13
|
+
}
|
package/src/models.json
CHANGED
|
@@ -10978,35 +10978,6 @@
|
|
|
10978
10978
|
]
|
|
10979
10979
|
}
|
|
10980
10980
|
},
|
|
10981
|
-
"gemini-3.1-pro-high": {
|
|
10982
|
-
"id": "gemini-3.1-pro-high",
|
|
10983
|
-
"name": "Gemini 3.1 Pro (High) (Antigravity)",
|
|
10984
|
-
"api": "google-gemini-cli",
|
|
10985
|
-
"provider": "google-antigravity",
|
|
10986
|
-
"baseUrl": "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
10987
|
-
"reasoning": true,
|
|
10988
|
-
"input": [
|
|
10989
|
-
"text",
|
|
10990
|
-
"image"
|
|
10991
|
-
],
|
|
10992
|
-
"cost": {
|
|
10993
|
-
"input": 0,
|
|
10994
|
-
"output": 0,
|
|
10995
|
-
"cacheRead": 0,
|
|
10996
|
-
"cacheWrite": 0
|
|
10997
|
-
},
|
|
10998
|
-
"contextWindow": 1048576,
|
|
10999
|
-
"maxTokens": 65535,
|
|
11000
|
-
"thinking": {
|
|
11001
|
-
"mode": "google-level",
|
|
11002
|
-
"minLevel": "low",
|
|
11003
|
-
"maxLevel": "high",
|
|
11004
|
-
"levels": [
|
|
11005
|
-
"low",
|
|
11006
|
-
"high"
|
|
11007
|
-
]
|
|
11008
|
-
}
|
|
11009
|
-
},
|
|
11010
10981
|
"gemini-3.1-pro-low": {
|
|
11011
10982
|
"id": "gemini-3.1-pro-low",
|
|
11012
10983
|
"name": "Gemini 3.1 Pro (Low) (Antigravity)",
|
package/src/models.ts
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { isRetiredModelKey } from "./model-retirements";
|
|
1
3
|
import { applyGeneratedModelPolicies, enrichModelThinking } from "./model-thinking";
|
|
2
|
-
import MODELS from "./models.json" with { type: "json" };
|
|
3
4
|
import type { Api, KnownProvider, Model, Usage } from "./types";
|
|
4
5
|
import { isClaudeForcedToolChoiceIncapableModelId } from "./utils/tool-choice-capability";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Static bundled model registry loaded from `models.json`.
|
|
8
|
+
* Static bundled model registry loaded lazily from `models.json`.
|
|
8
9
|
*
|
|
9
10
|
* This module intentionally exposes compile-time defaults only.
|
|
10
11
|
* It does not include runtime discovery, models.dev overlays, or on-disk cache state.
|
|
11
12
|
*
|
|
12
13
|
* For runtime-aware resolution, use `createModelManager()` / `resolveProviderModels()`.
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
type BundledCatalog = typeof import("./models.json");
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const COMPILED_MODELS_PATH = "./packages/ai/src/models.json";
|
|
19
|
+
let bundledCatalog: BundledCatalog | undefined;
|
|
20
|
+
let providerNames: KnownProvider[] | undefined;
|
|
15
21
|
const providerModelRegistry: Map<string, Map<string, Model<Api>>> = new Map();
|
|
16
22
|
|
|
23
|
+
function getBundledCatalog(): BundledCatalog {
|
|
24
|
+
bundledCatalog ??= require(
|
|
25
|
+
process.env.PI_COMPILED === "true" ? COMPILED_MODELS_PATH : "./models.json",
|
|
26
|
+
) as BundledCatalog;
|
|
27
|
+
return bundledCatalog;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
function getProviderModels(provider: GeneratedProvider): Map<string, Model<Api>> | undefined {
|
|
18
31
|
const cached = providerModelRegistry.get(provider);
|
|
19
32
|
if (cached) return cached;
|
|
20
|
-
const models =
|
|
33
|
+
const models = getBundledCatalog()[provider];
|
|
21
34
|
if (!models) return undefined;
|
|
22
35
|
const providerModels = new Map<string, Model<Api>>();
|
|
23
36
|
for (const [id, model] of Object.entries(models)) {
|
|
37
|
+
if (isRetiredModelKey(provider, id)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
24
40
|
providerModels.set(id, applyBundledCompatDefaults(enrichModelThinking(model as Model<Api>)));
|
|
25
41
|
}
|
|
26
42
|
providerModelRegistry.set(provider, providerModels);
|
|
@@ -52,7 +68,7 @@ function applyBundledCompatDefaults(model: Model<Api>): Model<Api> {
|
|
|
52
68
|
return policyModels[0] ?? normalized;
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
export type GeneratedProvider = keyof
|
|
71
|
+
export type GeneratedProvider = keyof BundledCatalog;
|
|
56
72
|
|
|
57
73
|
export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi> {
|
|
58
74
|
const providerModels = getProviderModels(provider);
|
|
@@ -62,6 +78,7 @@ export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvi
|
|
|
62
78
|
export function getBundledProviders(): KnownProvider[] {
|
|
63
79
|
// Defensive copy: the old eager path returned a fresh Array.from(...), so
|
|
64
80
|
// callers may freely mutate their result without corrupting enumeration.
|
|
81
|
+
providerNames ??= Object.keys(getBundledCatalog()) as KnownProvider[];
|
|
65
82
|
return providerNames.slice();
|
|
66
83
|
}
|
|
67
84
|
|
|
@@ -1513,7 +1513,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1513
1513
|
partial: output,
|
|
1514
1514
|
});
|
|
1515
1515
|
} else if (block.type === "toolCall") {
|
|
1516
|
-
|
|
1516
|
+
if (block.partialJson.trim()) {
|
|
1517
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
1518
|
+
}
|
|
1517
1519
|
delete (block as { partialJson?: string }).partialJson;
|
|
1518
1520
|
stream.push({
|
|
1519
1521
|
type: "toolcall_end",
|
|
@@ -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.50.0";
|
|
9
9
|
|
|
10
10
|
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
11
11
|
const version =
|
package/src/stream.ts
CHANGED
|
@@ -12,22 +12,15 @@ import {
|
|
|
12
12
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
13
13
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
14
14
|
import type { CursorOptions } from "./providers/cursor";
|
|
15
|
-
import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
|
|
16
15
|
import type { GoogleOptions } from "./providers/google";
|
|
17
16
|
import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
|
|
18
17
|
import type { GoogleVertexOptions } from "./providers/google-vertex";
|
|
19
|
-
import { isKimiModel, streamKimi } from "./providers/kimi";
|
|
20
18
|
import type { OllamaChatOptions } from "./providers/ollama";
|
|
21
19
|
import type { OpenAICompletionsOptions } from "./providers/openai-completions";
|
|
22
|
-
import { streamPiNative } from "./providers/pi-native-client";
|
|
23
20
|
// Heavy provider stream functions are imported lazily via register-builtins,
|
|
24
|
-
// which wraps each provider module in a dynamic import.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// gitlab-duo / kimi / synthetic providers stay eager because their modules
|
|
28
|
-
// export routing predicates (isGitLabDuoModel, isKimiModel, isSyntheticModel)
|
|
29
|
-
// that must be callable synchronously before streaming begins, and their
|
|
30
|
-
// modules are thin wrappers with no heavy SDK dependencies.
|
|
21
|
+
// which wraps each provider module in a dynamic import. Thin provider routing
|
|
22
|
+
// modules are also loaded lazily below by returning an outer stream and piping
|
|
23
|
+
// the dynamically imported inner stream into it.
|
|
31
24
|
import {
|
|
32
25
|
streamAnthropic,
|
|
33
26
|
streamAzureOpenAIResponses,
|
|
@@ -41,7 +34,6 @@ import {
|
|
|
41
34
|
streamOpenAICompletions,
|
|
42
35
|
streamOpenAIResponses,
|
|
43
36
|
} from "./providers/register-builtins";
|
|
44
|
-
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
|
|
45
37
|
import type {
|
|
46
38
|
Api,
|
|
47
39
|
AssistantMessage,
|
|
@@ -240,6 +232,45 @@ export function formatProviderCredentialHint(provider: string): string {
|
|
|
240
232
|
}
|
|
241
233
|
return parts.join(" ");
|
|
242
234
|
}
|
|
235
|
+
function pipeAssistantStream(
|
|
236
|
+
outer: AssistantMessageEventStream,
|
|
237
|
+
inner: AssistantMessageEventStream,
|
|
238
|
+
signal?: AbortSignal,
|
|
239
|
+
): void {
|
|
240
|
+
void (async () => {
|
|
241
|
+
try {
|
|
242
|
+
for await (const event of inner) {
|
|
243
|
+
outer.push(event);
|
|
244
|
+
// The inner provider stream owns abort semantics (it receives the
|
|
245
|
+
// same signal), but stop forwarding as soon as the consumer
|
|
246
|
+
// aborted so a misbehaving inner stream cannot keep the pipe
|
|
247
|
+
// buffering events indefinitely.
|
|
248
|
+
if (signal?.aborted && !outer.done) {
|
|
249
|
+
outer.end(await inner.result());
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!outer.done) outer.end(await inner.result());
|
|
254
|
+
} catch (error) {
|
|
255
|
+
outer.fail(error);
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function streamFromLazyImport(
|
|
261
|
+
createInner: () => Promise<AssistantMessageEventStream>,
|
|
262
|
+
signal?: AbortSignal,
|
|
263
|
+
): AssistantMessageEventStream {
|
|
264
|
+
const outer = new AssistantMessageEventStream();
|
|
265
|
+
void (async () => {
|
|
266
|
+
try {
|
|
267
|
+
pipeAssistantStream(outer, await createInner(), signal);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
outer.fail(error);
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
272
|
+
return outer;
|
|
273
|
+
}
|
|
243
274
|
|
|
244
275
|
/**
|
|
245
276
|
* Build an actionable "missing API key" error for a provider, used by the
|
|
@@ -262,15 +293,21 @@ export function stream<TApi extends Api>(
|
|
|
262
293
|
return customApiProvider.stream(model, context, options as StreamOptions);
|
|
263
294
|
}
|
|
264
295
|
|
|
265
|
-
if (
|
|
296
|
+
if (model.provider === "gitlab-duo") {
|
|
266
297
|
const apiKey = (options as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
|
|
267
298
|
if (!apiKey) {
|
|
268
299
|
throw new Error(formatMissingApiKeyError(model.provider));
|
|
269
300
|
}
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
301
|
+
return streamFromLazyImport(
|
|
302
|
+
async () => {
|
|
303
|
+
const { streamGitLabDuo } = await import("./providers/gitlab-duo");
|
|
304
|
+
return streamGitLabDuo(model, context, {
|
|
305
|
+
...(options as SimpleStreamOptions | undefined),
|
|
306
|
+
apiKey,
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
(options as StreamOptions | undefined)?.signal,
|
|
310
|
+
);
|
|
274
311
|
}
|
|
275
312
|
|
|
276
313
|
// Vertex AI uses Application Default Credentials, not API keys
|
|
@@ -446,7 +483,10 @@ export function streamSimple<TApi extends Api>(
|
|
|
446
483
|
// extension-registered APIs can't accidentally override a configured
|
|
447
484
|
// pi-native transport.
|
|
448
485
|
if (model.transport === "pi-native") {
|
|
449
|
-
return
|
|
486
|
+
return streamFromLazyImport(async () => {
|
|
487
|
+
const { streamPiNative } = await import("./providers/pi-native-client");
|
|
488
|
+
return streamPiNative(model, context, options);
|
|
489
|
+
}, options?.signal);
|
|
450
490
|
}
|
|
451
491
|
|
|
452
492
|
// Check custom API registry (extension-provided APIs)
|
|
@@ -471,31 +511,40 @@ export function streamSimple<TApi extends Api>(
|
|
|
471
511
|
}
|
|
472
512
|
|
|
473
513
|
// GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
|
|
474
|
-
if (
|
|
475
|
-
return
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
514
|
+
if (model.provider === "gitlab-duo") {
|
|
515
|
+
return streamFromLazyImport(async () => {
|
|
516
|
+
const { streamGitLabDuo } = await import("./providers/gitlab-duo");
|
|
517
|
+
return streamGitLabDuo(model, context, {
|
|
518
|
+
...options,
|
|
519
|
+
apiKey,
|
|
520
|
+
});
|
|
521
|
+
}, options?.signal);
|
|
479
522
|
}
|
|
480
523
|
|
|
481
524
|
// Kimi Code - route to dedicated handler that wraps OpenAI or Anthropic API
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
525
|
+
if (model.provider === "kimi-code") {
|
|
526
|
+
return streamFromLazyImport(async () => {
|
|
527
|
+
const { streamKimi } = await import("./providers/kimi");
|
|
528
|
+
// Pass raw SimpleStreamOptions - streamKimi handles mapping internally
|
|
529
|
+
return streamKimi(model as Model<"openai-completions">, context, {
|
|
530
|
+
...options,
|
|
531
|
+
apiKey,
|
|
532
|
+
format: options?.kimiApiFormat ?? "anthropic",
|
|
533
|
+
});
|
|
534
|
+
}, options?.signal);
|
|
489
535
|
}
|
|
490
536
|
|
|
491
537
|
// Synthetic - route to dedicated handler that wraps OpenAI or Anthropic API
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
538
|
+
if (model.provider === "synthetic") {
|
|
539
|
+
return streamFromLazyImport(async () => {
|
|
540
|
+
const { streamSynthetic } = await import("./providers/synthetic");
|
|
541
|
+
// Pass raw SimpleStreamOptions - streamSynthetic handles mapping internally
|
|
542
|
+
return streamSynthetic(model as Model<"openai-completions">, context, {
|
|
543
|
+
...options,
|
|
544
|
+
apiKey,
|
|
545
|
+
format: options?.syntheticApiFormat ?? "openai", // Default to OpenAI format
|
|
546
|
+
});
|
|
547
|
+
}, options?.signal);
|
|
499
548
|
}
|
|
500
549
|
|
|
501
550
|
const providerOptions = mapOptionsForApi(model, options, apiKey);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
|
+
import { isRetiredModelKey } from "../../model-retirements";
|
|
2
3
|
import { getAntigravityUserAgent } from "../../providers/google-gemini-headers";
|
|
3
4
|
import type { Model } from "../../types";
|
|
4
|
-
import { toPositiveNumber } from "../../utils";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS = [
|
|
7
7
|
"https://daily-cloudcode-pa.googleapis.com",
|
|
@@ -162,6 +162,12 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
162
162
|
signal?: AbortSignal;
|
|
163
163
|
/** Optional fetch implementation override for tests. */
|
|
164
164
|
fetcher?: typeof fetch;
|
|
165
|
+
/**
|
|
166
|
+
* Provider id the caller assigns to returned models. Scopes retired-selector
|
|
167
|
+
* filtering (e.g. `google-gemini-cli` reuses this helper and remaps rows).
|
|
168
|
+
* Default: `google-antigravity`.
|
|
169
|
+
*/
|
|
170
|
+
targetProvider?: "google-antigravity" | "google-gemini-cli";
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
/**
|
|
@@ -174,6 +180,7 @@ export async function fetchAntigravityDiscoveryModels(
|
|
|
174
180
|
options: FetchAntigravityDiscoveryModelsOptions,
|
|
175
181
|
): Promise<Model<"google-gemini-cli">[] | null> {
|
|
176
182
|
const fetcher = options.fetcher ?? fetch;
|
|
183
|
+
const targetProvider = options.targetProvider ?? "google-antigravity";
|
|
177
184
|
const endpoints = options.endpoint
|
|
178
185
|
? [trimTrailingSlashes(options.endpoint)]
|
|
179
186
|
: DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS.map(trimTrailingSlashes);
|
|
@@ -214,7 +221,7 @@ export async function fetchAntigravityDiscoveryModels(
|
|
|
214
221
|
const models: Model<"google-gemini-cli">[] = [];
|
|
215
222
|
|
|
216
223
|
for (const [modelId, model] of Object.entries(parsed.models ?? {})) {
|
|
217
|
-
if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId)) {
|
|
224
|
+
if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId) || isRetiredModelKey(targetProvider, modelId)) {
|
|
218
225
|
continue;
|
|
219
226
|
}
|
|
220
227
|
if (model.isInternal === true) {
|
|
@@ -256,6 +263,13 @@ function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscovery
|
|
|
256
263
|
return parsed.data;
|
|
257
264
|
}
|
|
258
265
|
|
|
266
|
+
function toPositiveNumber(value: unknown, fallback: number): number {
|
|
267
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
268
|
+
return fallback;
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
|
|
259
273
|
function trimTrailingSlashes(value: string): string {
|
|
260
274
|
return value.replace(/\/+$/, "");
|
|
261
275
|
}
|