refactor: standardize runtime guard patterns with lodash-es (#154)
* refactor(ui): replace runtime type guards with lodash-es predicates * refactor(runtime): migrate typeof checks to lodash-es in core modules * refactor(ipc): standardize runtime guards with lodash-es * test(refactor): align runtime guard checks with lodash-es * docs(agents): add lodash-es runtime guard skill summary
This commit is contained in:
33 files changed
+303
-208
No files matched your search
@@ -0,0 +1,74 @@
|
|||||||
|
# lodash-es-runtime-guards
|
||||||
|
|
||||||
|
Replace runtime `typeof` checks and ad-hoc empty-string checks with `lodash-es` predicates.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- You see runtime checks like `typeof value === 'string'`.
|
||||||
|
- You see object guards like `value && typeof value === 'object'`.
|
||||||
|
- You see emptiness checks like `value.trim() !== ''` or `value.trim() === ''`.
|
||||||
|
- You want consistent guard style across app, server, IPC, and tests.
|
||||||
|
|
||||||
|
## Do Not Replace
|
||||||
|
|
||||||
|
- Type-level `typeof` (TypeScript only), for example:
|
||||||
|
- `ReturnType<typeof fn>`
|
||||||
|
- `keyof typeof CONST_MAP`
|
||||||
|
- `z.infer<typeof Schema>`
|
||||||
|
|
||||||
|
These are compile-time types and must remain as-is.
|
||||||
|
|
||||||
|
## Guard Mapping
|
||||||
|
|
||||||
|
- `typeof x === 'string'` -> `isString(x)`
|
||||||
|
- `typeof x !== 'string'` -> `!isString(x)`
|
||||||
|
- `typeof x === 'number'` -> `isNumber(x)`
|
||||||
|
- `typeof x === 'boolean'` -> `isBoolean(x)`
|
||||||
|
- `typeof x === 'function'` -> `isFunction(x)`
|
||||||
|
- `typeof x === 'undefined'` -> `isUndefined(x)`
|
||||||
|
- `x && typeof x === 'object'` -> `isObjectLike(x)`
|
||||||
|
- `typeof x === 'object' && x !== null` -> `isObjectLike(x)`
|
||||||
|
- `typeof x === 'object' && !Array.isArray(x)` -> `isObjectLike(x) && !isArray(x)`
|
||||||
|
|
||||||
|
## String Emptiness Pattern
|
||||||
|
|
||||||
|
- Prefer:
|
||||||
|
- `isEmpty(value.trim())`
|
||||||
|
- Instead of:
|
||||||
|
- `value.trim() === ''`
|
||||||
|
- `value.trim() !== ''`
|
||||||
|
- `value.trim().length === 0`
|
||||||
|
|
||||||
|
## Import Style
|
||||||
|
|
||||||
|
- Use named imports from `lodash-es`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { isString, isNumber, isObjectLike, isEmpty } from 'lodash-es';
|
||||||
|
```
|
||||||
|
|
||||||
|
- Do not use full-package imports.
|
||||||
|
- Reuse existing imports in file; avoid duplicate imports.
|
||||||
|
|
||||||
|
## Refactor Checklist
|
||||||
|
|
||||||
|
1. Replace runtime checks first, preserving behavior.
|
||||||
|
2. Keep array/object branching semantics unchanged.
|
||||||
|
3. Re-scan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "typeof\s+[^\n]+(?:===|!==)\s*'" src
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Confirm only type-level `typeof` remains:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "\btypeof\b" src
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run type-check
|
||||||
|
```
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { filter, flatMap, isEmpty, size, sumBy } from 'lodash-es';
|
import { filter, flatMap, isEmpty, isNumber, size, sumBy } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
clampQuotaPercentage,
|
clampQuotaPercentage,
|
||||||
getQuotaStatus,
|
getQuotaStatus,
|
||||||
@@ -313,7 +313,7 @@ export function CloudAccountList() {
|
|||||||
{
|
{
|
||||||
onSuccess: (updatedAccount) => {
|
onSuccess: (updatedAccount) => {
|
||||||
const credits = updatedAccount.quota?.ai_credits?.credits;
|
const credits = updatedAccount.quota?.ai_credits?.credits;
|
||||||
if (typeof credits === 'number') {
|
if (isNumber(credits)) {
|
||||||
toast({
|
toast({
|
||||||
title: t('cloud.toast.quotaRefreshed'),
|
title: t('cloud.toast.quotaRefreshed'),
|
||||||
description: `AI credits: $${credits.toFixed(2)}`,
|
description: `AI credits: $${credits.toFixed(2)}`,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||||
|
import { isNumber, isObjectLike } from 'lodash-es';
|
||||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -52,11 +53,11 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
collisionPadding?: number | Partial<Record<'top' | 'right' | 'bottom' | 'left', number>>;
|
collisionPadding?: number | Partial<Record<'top' | 'right' | 'bottom' | 'left', number>>;
|
||||||
}
|
}
|
||||||
>(({ className, sideOffset = 4, collisionPadding = 8, ...props }, ref) => {
|
>(({ className, sideOffset = 4, collisionPadding = 8, ...props }, ref) => {
|
||||||
if (typeof collisionPadding === 'number') {
|
if (isNumber(collisionPadding)) {
|
||||||
if (collisionPadding < 0) {
|
if (collisionPadding < 0) {
|
||||||
throw new Error('collisionPadding must be a non-negative number');
|
throw new Error('collisionPadding must be a non-negative number');
|
||||||
}
|
}
|
||||||
} else if (typeof collisionPadding === 'object' && collisionPadding !== null) {
|
} else if (isObjectLike(collisionPadding)) {
|
||||||
for (const key of Object.keys(collisionPadding)) {
|
for (const key of Object.keys(collisionPadding)) {
|
||||||
if ((collisionPadding as any)[key] < 0) {
|
if ((collisionPadding as any)[key] < 0) {
|
||||||
throw new Error(`collisionPadding.${key} must be a non-negative number`);
|
throw new Error(`collisionPadding.${key} must be a non-negative number`);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { logger } from '../../utils/logger';
|
|||||||
|
|
||||||
import { shell } from 'electron';
|
import { shell } from 'electron';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { isEmpty, isString } from 'lodash-es';
|
||||||
import { updateTrayMenu } from '../../ipc/tray/handler';
|
import { updateTrayMenu } from '../../ipc/tray/handler';
|
||||||
import {
|
import {
|
||||||
ensureGlobalOriginalFromCurrentStorage,
|
ensureGlobalOriginalFromCurrentStorage,
|
||||||
@@ -46,7 +47,7 @@ function isEnterpriseClient(clientKey?: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeProjectId(projectId?: string): string | null {
|
function normalizeProjectId(projectId?: string): string | null {
|
||||||
if (typeof projectId !== 'string') {
|
if (!isString(projectId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const normalized = projectId.trim();
|
const normalized = projectId.trim();
|
||||||
@@ -148,7 +149,7 @@ async function clearAccountStatus(account: CloudAccount): Promise<void> {
|
|||||||
|
|
||||||
function hydrateActiveOAuthClientFromSettings(): void {
|
function hydrateActiveOAuthClientFromSettings(): void {
|
||||||
const preferredClientKey = CloudAccountRepo.getSetting<string>(ACTIVE_OAUTH_CLIENT_KEY_SETTING, '');
|
const preferredClientKey = CloudAccountRepo.getSetting<string>(ACTIVE_OAUTH_CLIENT_KEY_SETTING, '');
|
||||||
if (preferredClientKey && preferredClientKey.trim() !== '') {
|
if (isString(preferredClientKey) && !isEmpty(preferredClientKey.trim())) {
|
||||||
try {
|
try {
|
||||||
GoogleAPIService.setActiveOAuthClientKey(preferredClientKey);
|
GoogleAPIService.setActiveOAuthClientKey(preferredClientKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -195,7 +196,7 @@ async function backfillMissingOAuthClientKeyForLegacyAccounts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectMissing =
|
const projectMissing =
|
||||||
typeof account.token.project_id !== 'string' || account.token.project_id.trim() === '';
|
!isString(account.token.project_id) || isEmpty(account.token.project_id.trim());
|
||||||
if (activeClientKey === ENTERPRISE_OAUTH_CLIENT_KEY && projectMissing) {
|
if (activeClientKey === ENTERPRISE_OAUTH_CLIENT_KEY && projectMissing) {
|
||||||
skippedEnterpriseGuardCount += 1;
|
skippedEnterpriseGuardCount += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -232,7 +233,7 @@ export async function addGoogleAccount(
|
|||||||
oauthClientKey?: string,
|
oauthClientKey?: string,
|
||||||
): Promise<CloudAccount> {
|
): Promise<CloudAccount> {
|
||||||
try {
|
try {
|
||||||
if (oauthClientKey && oauthClientKey.trim() !== '') {
|
if (isString(oauthClientKey) && !isEmpty(oauthClientKey.trim())) {
|
||||||
setActiveOAuthClient(oauthClientKey);
|
setActiveOAuthClient(oauthClientKey);
|
||||||
} else {
|
} else {
|
||||||
hydrateActiveOAuthClientFromSettings();
|
hydrateActiveOAuthClientFromSettings();
|
||||||
@@ -684,7 +685,7 @@ export async function forcePollCloudMonitor(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function startAuthFlow(oauthClientKey?: string): Promise<void> {
|
export async function startAuthFlow(oauthClientKey?: string): Promise<void> {
|
||||||
if (oauthClientKey && oauthClientKey.trim() !== '') {
|
if (isString(oauthClientKey) && !isEmpty(oauthClientKey.trim())) {
|
||||||
setActiveOAuthClient(oauthClientKey);
|
setActiveOAuthClient(oauthClientKey);
|
||||||
} else {
|
} else {
|
||||||
hydrateActiveOAuthClientFromSettings();
|
hydrateActiveOAuthClientFromSettings();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { desc, eq } from 'drizzle-orm';
|
import { desc, eq } from 'drizzle-orm';
|
||||||
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
||||||
|
import { isNumber, isObjectLike, isPlainObject, isString } from 'lodash-es';
|
||||||
import { getCloudAccountsDbPath, getAntigravityDbPaths } from '../../utils/paths';
|
import { getCloudAccountsDbPath, getAntigravityDbPaths } from '../../utils/paths';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import {
|
import {
|
||||||
@@ -35,14 +36,14 @@ type DrizzleExecutor = Pick<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
function isSqliteBusyError(error: unknown): boolean {
|
function isSqliteBusyError(error: unknown): boolean {
|
||||||
if (!error || typeof error !== 'object') {
|
if (!isObjectLike(error)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const err = error as { code?: string; message?: string };
|
const err = error as { code?: string; message?: string };
|
||||||
if (err.code && SQLITE_BUSY_CODES.has(err.code)) {
|
if (err.code && SQLITE_BUSY_CODES.has(err.code)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (typeof err.message === 'string') {
|
if (isString(err.message)) {
|
||||||
return err.message.includes('SQLITE_BUSY') || err.message.includes('SQLITE_LOCKED');
|
return err.message.includes('SQLITE_BUSY') || err.message.includes('SQLITE_LOCKED');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -168,17 +169,13 @@ interface MigrationStats {
|
|||||||
failedFields: number;
|
failedFields: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readStringCandidate(
|
function readStringCandidate(
|
||||||
source: Record<string, unknown>,
|
source: Record<string, unknown>,
|
||||||
...keys: string[]
|
...keys: string[]
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const candidate = source[key];
|
const candidate = source[key];
|
||||||
if (typeof candidate === 'string' && candidate.length > 0) {
|
if (isString(candidate) && candidate.length > 0) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,14 +183,15 @@ function readStringCandidate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDeviceProfile(value: unknown): DeviceProfile | undefined {
|
function normalizeDeviceProfile(value: unknown): DeviceProfile | undefined {
|
||||||
if (!isRecord(value)) {
|
if (!isPlainObject(value)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const valueRecord = value as Record<string, unknown>;
|
||||||
|
|
||||||
const machineId = readStringCandidate(value, 'machineId', 'machine_id');
|
const machineId = readStringCandidate(valueRecord, 'machineId', 'machine_id');
|
||||||
const macMachineId = readStringCandidate(value, 'macMachineId', 'mac_machine_id');
|
const macMachineId = readStringCandidate(valueRecord, 'macMachineId', 'mac_machine_id');
|
||||||
const devDeviceId = readStringCandidate(value, 'devDeviceId', 'dev_device_id');
|
const devDeviceId = readStringCandidate(valueRecord, 'devDeviceId', 'dev_device_id');
|
||||||
const sqmId = readStringCandidate(value, 'sqmId', 'sqm_id');
|
const sqmId = readStringCandidate(valueRecord, 'sqmId', 'sqm_id');
|
||||||
|
|
||||||
if (!machineId || !macMachineId || !devDeviceId || !sqmId) {
|
if (!machineId || !macMachineId || !devDeviceId || !sqmId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -217,45 +215,47 @@ function areDeviceProfilesEqual(left: DeviceProfile, right: DeviceProfile): bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readVersionedProfilePayload(value: unknown): unknown {
|
function readVersionedProfilePayload(value: unknown): unknown {
|
||||||
if (!isRecord(value)) {
|
if (!isPlainObject(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (!('schemaVersion' in value)) {
|
const valueRecord = value as Record<string, unknown>;
|
||||||
|
if (!('schemaVersion' in valueRecord)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaVersion = value.schemaVersion;
|
const schemaVersion = valueRecord.schemaVersion;
|
||||||
if (typeof schemaVersion !== 'number' || !Number.isFinite(schemaVersion)) {
|
if (!isNumber(schemaVersion) || !Number.isFinite(schemaVersion)) {
|
||||||
throw new Error('invalid_device_profile_schema_version');
|
throw new Error('invalid_device_profile_schema_version');
|
||||||
}
|
}
|
||||||
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
|
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
|
||||||
throw new Error(`unsupported_device_profile_schema_version:${schemaVersion}`);
|
throw new Error(`unsupported_device_profile_schema_version:${schemaVersion}`);
|
||||||
}
|
}
|
||||||
if (!('profile' in value)) {
|
if (!('profile' in valueRecord)) {
|
||||||
throw new Error('invalid_device_profile_payload');
|
throw new Error('invalid_device_profile_payload');
|
||||||
}
|
}
|
||||||
return value.profile;
|
return valueRecord.profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readVersionedHistoryPayload(value: unknown): unknown {
|
function readVersionedHistoryPayload(value: unknown): unknown {
|
||||||
if (!isRecord(value)) {
|
if (!isPlainObject(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (!('schemaVersion' in value)) {
|
const valueRecord = value as Record<string, unknown>;
|
||||||
|
if (!('schemaVersion' in valueRecord)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaVersion = value.schemaVersion;
|
const schemaVersion = valueRecord.schemaVersion;
|
||||||
if (typeof schemaVersion !== 'number' || !Number.isFinite(schemaVersion)) {
|
if (!isNumber(schemaVersion) || !Number.isFinite(schemaVersion)) {
|
||||||
throw new Error('invalid_device_history_schema_version');
|
throw new Error('invalid_device_history_schema_version');
|
||||||
}
|
}
|
||||||
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
|
if (schemaVersion !== DEVICE_PAYLOAD_SCHEMA_VERSION) {
|
||||||
throw new Error(`unsupported_device_history_schema_version:${schemaVersion}`);
|
throw new Error(`unsupported_device_history_schema_version:${schemaVersion}`);
|
||||||
}
|
}
|
||||||
if (!('history' in value)) {
|
if (!('history' in valueRecord)) {
|
||||||
throw new Error('invalid_device_history_payload');
|
throw new Error('invalid_device_history_payload');
|
||||||
}
|
}
|
||||||
return value.history;
|
return valueRecord.history;
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeDeviceProfile(profile: DeviceProfile | undefined): string | null {
|
function serializeDeviceProfile(profile: DeviceProfile | undefined): string | null {
|
||||||
@@ -285,23 +285,25 @@ function normalizeDeviceHistory(value: unknown): DeviceProfileVersion[] | undefi
|
|||||||
|
|
||||||
const normalized: DeviceProfileVersion[] = [];
|
const normalized: DeviceProfileVersion[] = [];
|
||||||
for (const item of value) {
|
for (const item of value) {
|
||||||
if (!isRecord(item)) {
|
if (!isPlainObject(item)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const itemRecord = item as Record<string, unknown>;
|
||||||
|
|
||||||
const profile = normalizeDeviceProfile(item.profile);
|
const profile = normalizeDeviceProfile(itemRecord.profile);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = typeof item.id === 'string' && item.id.length > 0 ? item.id : uuidv4();
|
const id = isString(itemRecord.id) && itemRecord.id.length > 0 ? itemRecord.id : uuidv4();
|
||||||
const createdAtCandidate = item.createdAt;
|
const createdAtCandidate = itemRecord.createdAt;
|
||||||
const createdAt =
|
const createdAt =
|
||||||
typeof createdAtCandidate === 'number' && Number.isFinite(createdAtCandidate)
|
isNumber(createdAtCandidate) && Number.isFinite(createdAtCandidate)
|
||||||
? Math.floor(createdAtCandidate)
|
? Math.floor(createdAtCandidate)
|
||||||
: Math.floor(Date.now() / 1000);
|
: Math.floor(Date.now() / 1000);
|
||||||
const label = typeof item.label === 'string' && item.label.length > 0 ? item.label : 'legacy';
|
const label =
|
||||||
const isCurrent = item.isCurrent === true;
|
isString(itemRecord.label) && itemRecord.label.length > 0 ? itemRecord.label : 'legacy';
|
||||||
|
const isCurrent = itemRecord.isCurrent === true;
|
||||||
|
|
||||||
normalized.push({
|
normalized.push({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { isString } from 'lodash-es';
|
||||||
import { AccountBackupData, AccountInfo } from '../../types/account';
|
import { AccountBackupData, AccountInfo } from '../../types/account';
|
||||||
import { ItemTableValueRowSchema, type ItemTableKey } from '../../types/db';
|
import { ItemTableValueRowSchema, type ItemTableKey } from '../../types/db';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
@@ -192,8 +193,8 @@ export function getCurrentAccountInfo(): AccountInfo {
|
|||||||
// Helper to find email in object
|
// Helper to find email in object
|
||||||
const findEmail = (obj: { email?: string; user?: { email?: string } }): string => {
|
const findEmail = (obj: { email?: string; user?: { email?: string } }): string => {
|
||||||
if (!obj) return '';
|
if (!obj) return '';
|
||||||
if (typeof obj.email === 'string') return obj.email;
|
if (isString(obj.email)) return obj.email;
|
||||||
if (obj.user && typeof obj.user.email === 'string') return obj.user.email;
|
if (obj.user && isString(obj.user.email)) return obj.user.email;
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -323,7 +324,7 @@ function _restoreSingleDb(dbPath: string, backup: AccountBackupData): boolean {
|
|||||||
for (const key of KEYS_TO_BACKUP) {
|
for (const key of KEYS_TO_BACKUP) {
|
||||||
if (key in backup.data) {
|
if (key in backup.data) {
|
||||||
const value = backup.data[key];
|
const value = backup.data[key];
|
||||||
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
const stringValue = isString(value) ? value : JSON.stringify(value);
|
||||||
tx.insert(itemTable)
|
tx.insert(itemTable)
|
||||||
.values({ key, value: stringValue })
|
.values({ key, value: stringValue })
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
|
|||||||
+11
-10
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createHash, randomBytes } from 'crypto';
|
import { createHash, randomBytes } from 'crypto';
|
||||||
|
import { isBoolean, isNumber, isObjectLike, isString } from 'lodash-es';
|
||||||
import { type DeviceProfile } from '../../types/account';
|
import { type DeviceProfile } from '../../types/account';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { getAgentDir, getAntigravityDbPaths, getAntigravityStoragePaths } from '../../utils/paths';
|
import { getAgentDir, getAntigravityDbPaths, getAntigravityStoragePaths } from '../../utils/paths';
|
||||||
@@ -76,14 +77,14 @@ const deviceHardeningState: DeviceHardeningState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function isSqliteBusyError(error: unknown): boolean {
|
function isSqliteBusyError(error: unknown): boolean {
|
||||||
if (!error || typeof error !== 'object') {
|
if (!isObjectLike(error)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const candidate = error as { code?: string; message?: string };
|
const candidate = error as { code?: string; message?: string };
|
||||||
if (candidate.code === 'SQLITE_BUSY' || candidate.code === 'SQLITE_LOCKED') {
|
if (candidate.code === 'SQLITE_BUSY' || candidate.code === 'SQLITE_LOCKED') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (typeof candidate.message === 'string') {
|
if (isString(candidate.message)) {
|
||||||
return candidate.message.includes('SQLITE_BUSY') || candidate.message.includes('SQLITE_LOCKED');
|
return candidate.message.includes('SQLITE_BUSY') || candidate.message.includes('SQLITE_LOCKED');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -139,8 +140,8 @@ function readLastKnownGoodMarker(): LastKnownGoodMarker | null {
|
|||||||
if (
|
if (
|
||||||
parsed &&
|
parsed &&
|
||||||
parsed.version === 1 &&
|
parsed.version === 1 &&
|
||||||
typeof parsed.savedAt === 'number' &&
|
isNumber(parsed.savedAt) &&
|
||||||
typeof parsed.hasStateDb === 'boolean'
|
isBoolean(parsed.hasStateDb)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -157,7 +158,7 @@ function readLastKnownGoodMarker(): LastKnownGoodMarker | null {
|
|||||||
function parseStorageJson(storagePath: string): Record<string, unknown> {
|
function parseStorageJson(storagePath: string): Record<string, unknown> {
|
||||||
const content = fs.readFileSync(storagePath, 'utf-8');
|
const content = fs.readFileSync(storagePath, 'utf-8');
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
if (!isObjectLike(parsed) || Array.isArray(parsed)) {
|
||||||
throw new Error('storage.json top-level value must be an object');
|
throw new Error('storage.json top-level value must be an object');
|
||||||
}
|
}
|
||||||
return parsed as Record<string, unknown>;
|
return parsed as Record<string, unknown>;
|
||||||
@@ -165,15 +166,15 @@ function parseStorageJson(storagePath: string): Record<string, unknown> {
|
|||||||
|
|
||||||
function getTelemetryField(storage: Record<string, unknown>, key: string): string | null {
|
function getTelemetryField(storage: Record<string, unknown>, key: string): string | null {
|
||||||
const telemetry = storage.telemetry;
|
const telemetry = storage.telemetry;
|
||||||
if (telemetry && typeof telemetry === 'object' && !Array.isArray(telemetry)) {
|
if (isObjectLike(telemetry) && !Array.isArray(telemetry)) {
|
||||||
const nestedValue = (telemetry as Record<string, unknown>)[key];
|
const nestedValue = (telemetry as Record<string, unknown>)[key];
|
||||||
if (typeof nestedValue === 'string' && nestedValue.length > 0) {
|
if (isString(nestedValue) && nestedValue.length > 0) {
|
||||||
return nestedValue;
|
return nestedValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const flatValue = storage[`telemetry.${key}`];
|
const flatValue = storage[`telemetry.${key}`];
|
||||||
if (typeof flatValue === 'string' && flatValue.length > 0) {
|
if (isString(flatValue) && flatValue.length > 0) {
|
||||||
return flatValue;
|
return flatValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ function getTelemetryField(storage: Record<string, unknown>, key: string): strin
|
|||||||
function ensureTelemetryObject(storage: Record<string, unknown>): Record<string, unknown> {
|
function ensureTelemetryObject(storage: Record<string, unknown>): Record<string, unknown> {
|
||||||
if (
|
if (
|
||||||
!storage.telemetry ||
|
!storage.telemetry ||
|
||||||
typeof storage.telemetry !== 'object' ||
|
!isObjectLike(storage.telemetry) ||
|
||||||
Array.isArray(storage.telemetry)
|
Array.isArray(storage.telemetry)
|
||||||
) {
|
) {
|
||||||
storage.telemetry = {};
|
storage.telemetry = {};
|
||||||
@@ -223,7 +224,7 @@ function readStateServiceMachineIdValue(dbPath: string): string | null {
|
|||||||
const row = db
|
const row = db
|
||||||
.prepare("SELECT value FROM ItemTable WHERE key = 'storage.serviceMachineId' LIMIT 1")
|
.prepare("SELECT value FROM ItemTable WHERE key = 'storage.serviceMachineId' LIMIT 1")
|
||||||
.get() as { value?: unknown } | undefined;
|
.get() as { value?: unknown } | undefined;
|
||||||
if (!row || typeof row.value !== 'string' || row.value.length === 0) {
|
if (!row || !isString(row.value) || row.value.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return row.value;
|
return row.value;
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import { IPC_CHANNELS } from '@/constants';
|
import { IPC_CHANNELS } from '@/constants';
|
||||||
|
import { isString } from 'lodash-es';
|
||||||
|
|
||||||
// Custom IPC Client that correctly builds method paths
|
// Custom IPC Client that correctly builds method paths
|
||||||
function createIPCClient(port: MessagePort) {
|
function createIPCClient(port: MessagePort) {
|
||||||
@@ -10,7 +11,7 @@ function createIPCClient(port: MessagePort) {
|
|||||||
|
|
||||||
port.onmessage = (event) => {
|
port.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
const data = isString(event.data) ? JSON.parse(event.data) : event.data;
|
||||||
|
|
||||||
const id = data.i || data.id;
|
const id = data.i || data.id;
|
||||||
const pending = pendingRequests.get(id);
|
const pending = pendingRequests.get(id);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { exec, execSync, spawn } from 'child_process';
|
import { exec, execSync, spawn } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import findProcess, { ProcessInfo } from 'find-process';
|
import findProcess, { ProcessInfo } from 'find-process';
|
||||||
|
import { isNumber } from 'lodash-es';
|
||||||
import { getAntigravityExecutablePath, isWsl } from '../../utils/paths';
|
import { getAntigravityExecutablePath, isWsl } from '../../utils/paths';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ export async function isProcessRunning(): Promise<boolean> {
|
|||||||
|
|
||||||
const processMap = new Map<number, ProcessInfo>();
|
const processMap = new Map<number, ProcessInfo>();
|
||||||
for (const proc of allMatches) {
|
for (const proc of allMatches) {
|
||||||
if (typeof proc.pid === 'number') {
|
if (isNumber(proc.pid)) {
|
||||||
processMap.set(proc.pid, proc);
|
processMap.set(proc.pid, proc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { isPlainObject, isString } from 'lodash-es';
|
import { isEmpty, isPlainObject, isString } from 'lodash-es';
|
||||||
import { mapClaudeModelToGemini, normalizeGeminiModelAlias } from './ModelMapping';
|
import { mapClaudeModelToGemini, normalizeGeminiModelAlias } from './ModelMapping';
|
||||||
import { getMaxOutputTokens, getThinkingBudget } from './ModelSpecs';
|
import { getMaxOutputTokens, getThinkingBudget } from './ModelSpecs';
|
||||||
import { cleanJsonSchema, normalizeObjectJsonSchema } from './JsonSchemaUtils';
|
import { cleanJsonSchema, normalizeObjectJsonSchema } from './JsonSchemaUtils';
|
||||||
@@ -518,7 +518,7 @@ function buildContents(
|
|||||||
|
|
||||||
for (const block of contentBlocks) {
|
for (const block of contentBlocks) {
|
||||||
if (block.type === 'text') {
|
if (block.type === 'text') {
|
||||||
if (block.text && block.text !== '(no content)' && block.text.trim() !== '')
|
if (block.text && block.text !== '(no content)' && !isEmpty(block.text.trim()))
|
||||||
parts.push({ text: block.text.trim() });
|
parts.push({ text: block.text.trim() });
|
||||||
} else if (block.type === 'thinking') {
|
} else if (block.type === 'thinking') {
|
||||||
const part: any = { text: block.thinking, thought: true };
|
const part: any = { text: block.thinking, thought: true };
|
||||||
@@ -553,7 +553,7 @@ function buildContents(
|
|||||||
.filter((b: any) => b.type === 'text')
|
.filter((b: any) => b.type === 'text')
|
||||||
.map((b: any) => b.text)
|
.map((b: any) => b.text)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
if (mergedContent.trim().length === 0)
|
if (isEmpty(mergedContent.trim()))
|
||||||
mergedContent = block.is_error
|
mergedContent = block.is_error
|
||||||
? 'Tool execution failed with no output.'
|
? 'Tool execution failed with no output.'
|
||||||
: 'Command executed successfully.';
|
: 'Command executed successfully.';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isArray, isBoolean, isNumber, isObjectLike, isString } from 'lodash-es';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively cleans JSON Schema to meet Gemini interface requirements
|
* Recursively cleans JSON Schema to meet Gemini interface requirements
|
||||||
*
|
*
|
||||||
@@ -9,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
export function cleanJsonSchema(value: any) {
|
export function cleanJsonSchema(value: any) {
|
||||||
// 0. Preprocessing: Expand $ref (Schema Flattening)
|
// 0. Preprocessing: Expand $ref (Schema Flattening)
|
||||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
if (isObjectLike(value) && !isArray(value)) {
|
||||||
const defs: Record<string, any> = {};
|
const defs: Record<string, any> = {};
|
||||||
|
|
||||||
// Extract $defs or definitions
|
// Extract $defs or definitions
|
||||||
@@ -34,21 +36,21 @@ export function cleanJsonSchema(value: any) {
|
|||||||
|
|
||||||
export function normalizeObjectJsonSchema(schema: unknown): Record<string, unknown> {
|
export function normalizeObjectJsonSchema(schema: unknown): Record<string, unknown> {
|
||||||
const fallbackSchema: Record<string, unknown> = { type: 'object', properties: {} };
|
const fallbackSchema: Record<string, unknown> = { type: 'object', properties: {} };
|
||||||
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
if (!isObjectLike(schema) || isArray(schema)) {
|
||||||
return fallbackSchema;
|
return fallbackSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedSchema = JSON.parse(JSON.stringify(schema)) as Record<string, unknown>;
|
const normalizedSchema = JSON.parse(JSON.stringify(schema)) as Record<string, unknown>;
|
||||||
cleanJsonSchema(normalizedSchema);
|
cleanJsonSchema(normalizedSchema);
|
||||||
|
|
||||||
if (typeof normalizedSchema.type !== 'string') {
|
if (!isString(normalizedSchema.type)) {
|
||||||
normalizedSchema.type = 'object';
|
normalizedSchema.type = 'object';
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
normalizedSchema.type === 'object' &&
|
normalizedSchema.type === 'object' &&
|
||||||
(!normalizedSchema.properties ||
|
(!normalizedSchema.properties ||
|
||||||
typeof normalizedSchema.properties !== 'object' ||
|
!isObjectLike(normalizedSchema.properties) ||
|
||||||
Array.isArray(normalizedSchema.properties))
|
isArray(normalizedSchema.properties))
|
||||||
) {
|
) {
|
||||||
normalizedSchema.properties = {};
|
normalizedSchema.properties = {};
|
||||||
}
|
}
|
||||||
@@ -60,10 +62,10 @@ export function normalizeObjectJsonSchema(schema: unknown): Record<string, unkno
|
|||||||
* Recursively expand $ref
|
* Recursively expand $ref
|
||||||
*/
|
*/
|
||||||
function flattenRefs(map: any, defs: Record<string, any>) {
|
function flattenRefs(map: any, defs: Record<string, any>) {
|
||||||
if (!map || typeof map !== 'object') return;
|
if (!isObjectLike(map)) return;
|
||||||
|
|
||||||
// Check and replace $ref
|
// Check and replace $ref
|
||||||
if (typeof map['$ref'] === 'string') {
|
if (isString(map['$ref'])) {
|
||||||
const refPath = map['$ref'];
|
const refPath = map['$ref'];
|
||||||
// Parse reference name (e.g. #/$defs/MyType -> MyType)
|
// Parse reference name (e.g. #/$defs/MyType -> MyType)
|
||||||
const parts = refPath.split('/');
|
const parts = refPath.split('/');
|
||||||
@@ -74,7 +76,7 @@ function flattenRefs(map: any, defs: Record<string, any>) {
|
|||||||
// $ref nodes should not have other properties, remove $ref directly
|
// $ref nodes should not have other properties, remove $ref directly
|
||||||
delete map['$ref'];
|
delete map['$ref'];
|
||||||
|
|
||||||
if (defSchema && typeof defSchema === 'object') {
|
if (isObjectLike(defSchema)) {
|
||||||
for (const [k, v] of Object.entries(defSchema)) {
|
for (const [k, v] of Object.entries(defSchema)) {
|
||||||
// Only insert if the key does not exist in current map (avoid overwrite)
|
// Only insert if the key does not exist in current map (avoid overwrite)
|
||||||
if (map[k] === undefined) {
|
if (map[k] === undefined) {
|
||||||
@@ -93,7 +95,7 @@ function flattenRefs(map: any, defs: Record<string, any>) {
|
|||||||
for (const k in map) {
|
for (const k in map) {
|
||||||
if (Object.prototype.hasOwnProperty.call(map, k)) {
|
if (Object.prototype.hasOwnProperty.call(map, k)) {
|
||||||
const v = map[k];
|
const v = map[k];
|
||||||
if (typeof v === 'object' && v !== null) {
|
if (isObjectLike(v)) {
|
||||||
flattenRefs(v, defs);
|
flattenRefs(v, defs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,11 +103,11 @@ function flattenRefs(map: any, defs: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanJsonSchemaRecursive(value: any) {
|
function cleanJsonSchemaRecursive(value: any) {
|
||||||
if (!value || typeof value !== 'object') {
|
if (!isObjectLike(value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (isArray(value)) {
|
||||||
// Array: Recursively process each element
|
// Array: Recursively process each element
|
||||||
for (const v of value) {
|
for (const v of value) {
|
||||||
cleanJsonSchemaRecursive(v);
|
cleanJsonSchemaRecursive(v);
|
||||||
@@ -142,7 +144,7 @@ function cleanJsonSchemaRecursive(value: any) {
|
|||||||
if (map[field] !== undefined) {
|
if (map[field] !== undefined) {
|
||||||
const val = map[field];
|
const val = map[field];
|
||||||
// Only migrate if value is primitive type
|
// Only migrate if value is primitive type
|
||||||
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
if (isString(val) || isNumber(val) || isBoolean(val)) {
|
||||||
constraints.push(`${label}: ${val}`);
|
constraints.push(`${label}: ${val}`);
|
||||||
delete map[field];
|
delete map[field];
|
||||||
} else {
|
} else {
|
||||||
@@ -188,13 +190,13 @@ function cleanJsonSchemaRecursive(value: any) {
|
|||||||
// 5. Handle type field (Gemini requires single lowercase string)
|
// 5. Handle type field (Gemini requires single lowercase string)
|
||||||
if (map['type']) {
|
if (map['type']) {
|
||||||
const typeVal = map['type'];
|
const typeVal = map['type'];
|
||||||
if (typeof typeVal === 'string') {
|
if (isString(typeVal)) {
|
||||||
map['type'] = typeVal.toLowerCase();
|
map['type'] = typeVal.toLowerCase();
|
||||||
} else if (Array.isArray(typeVal)) {
|
} else if (isArray(typeVal)) {
|
||||||
// Union type downgrade: take the first non-null type
|
// Union type downgrade: take the first non-null type
|
||||||
let selectedType = 'string';
|
let selectedType = 'string';
|
||||||
for (const item of typeVal) {
|
for (const item of typeVal) {
|
||||||
if (typeof item === 'string' && item !== 'null') {
|
if (isString(item) && item !== 'null') {
|
||||||
selectedType = item.toLowerCase();
|
selectedType = item.toLowerCase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isEmpty, isString } from 'lodash-es';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
const PUBLIC_SUPPORTED_MODELS = [
|
const PUBLIC_SUPPORTED_MODELS = [
|
||||||
@@ -94,7 +95,7 @@ export function getSupportedModels(): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateDynamicForwardingRules(oldModel: string, newModel: string): void {
|
export function updateDynamicForwardingRules(oldModel: string, newModel: string): void {
|
||||||
if (typeof oldModel !== 'string' || typeof newModel !== 'string') {
|
if (!isString(oldModel) || !isString(newModel)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const normalizedOld = oldModel.trim();
|
const normalizedOld = oldModel.trim();
|
||||||
@@ -140,7 +141,7 @@ export function getAllDynamicModels(
|
|||||||
|
|
||||||
if (dynamicModelIds) {
|
if (dynamicModelIds) {
|
||||||
for (const dynamicModelId of dynamicModelIds) {
|
for (const dynamicModelId of dynamicModelIds) {
|
||||||
if (typeof dynamicModelId === 'string' && dynamicModelId.trim() !== '') {
|
if (isString(dynamicModelId) && !isEmpty(dynamicModelId.trim())) {
|
||||||
modelIds.add(dynamicModelId.trim());
|
modelIds.add(dynamicModelId.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +151,7 @@ export function getAllDynamicModels(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function mapClaudeModelToGemini(input: string): string {
|
export function mapClaudeModelToGemini(input: string): string {
|
||||||
if (!input || typeof input !== 'string') {
|
if (!isString(input) || isEmpty(input)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const mappedModel = CLAUDE_TO_GEMINI[input];
|
const mappedModel = CLAUDE_TO_GEMINI[input];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import modelSpecsJson from './model-specs';
|
import modelSpecsJson from './model-specs';
|
||||||
|
import { isNumber } from 'lodash-es';
|
||||||
|
|
||||||
type ModelSpec = {
|
type ModelSpec = {
|
||||||
max_output_tokens?: number;
|
max_output_tokens?: number;
|
||||||
@@ -24,7 +25,7 @@ export function resolveModelAlias(modelId: string): string {
|
|||||||
export function getMaxOutputTokens(modelId: string): number {
|
export function getMaxOutputTokens(modelId: string): number {
|
||||||
const resolved = resolveModelAlias(modelId);
|
const resolved = resolveModelAlias(modelId);
|
||||||
const fromSpec = SPECS.models[resolved]?.max_output_tokens;
|
const fromSpec = SPECS.models[resolved]?.max_output_tokens;
|
||||||
if (typeof fromSpec === 'number' && Number.isFinite(fromSpec) && fromSpec > 0) {
|
if (isNumber(fromSpec) && Number.isFinite(fromSpec) && fromSpec > 0) {
|
||||||
return Math.floor(fromSpec);
|
return Math.floor(fromSpec);
|
||||||
}
|
}
|
||||||
return DEFAULT_MAX_OUTPUT_TOKENS;
|
return DEFAULT_MAX_OUTPUT_TOKENS;
|
||||||
@@ -33,7 +34,7 @@ export function getMaxOutputTokens(modelId: string): number {
|
|||||||
export function getThinkingBudget(modelId: string): number {
|
export function getThinkingBudget(modelId: string): number {
|
||||||
const resolved = resolveModelAlias(modelId);
|
const resolved = resolveModelAlias(modelId);
|
||||||
const fromSpec = SPECS.models[resolved]?.thinking_budget;
|
const fromSpec = SPECS.models[resolved]?.thinking_budget;
|
||||||
if (typeof fromSpec === 'number' && Number.isFinite(fromSpec) && fromSpec >= 0) {
|
if (isNumber(fromSpec) && Number.isFinite(fromSpec) && fromSpec >= 0) {
|
||||||
return Math.floor(fromSpec);
|
return Math.floor(fromSpec);
|
||||||
}
|
}
|
||||||
return DEFAULT_THINKING_BUDGET;
|
return DEFAULT_THINKING_BUDGET;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { isNumber } from 'lodash-es';
|
||||||
import { QuotaData, LoadProjectResponse, QuotaApiResponse } from './types';
|
import { QuotaData, LoadProjectResponse, QuotaApiResponse } from './types';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ export class QuotaService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNextEndpoint && (status === 429 || (typeof status === 'number' && status >= 500))) {
|
if (hasNextEndpoint && (status === 429 || (isNumber(status) && status >= 500))) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Quota API ${endpoint} returned ${status}, falling back to next endpoint`,
|
`Quota API ${endpoint} returned ${status}, falling back to next endpoint`,
|
||||||
);
|
);
|
||||||
@@ -173,7 +174,7 @@ export class QuotaService {
|
|||||||
|
|
||||||
logger.warn(`API Error: ${status} - ${text}`);
|
logger.warn(`API Error: ${status} - ${text}`);
|
||||||
lastError = new Error(`HTTP ${status} - ${text}`);
|
lastError = new Error(`HTTP ${status} - ${text}`);
|
||||||
shouldFallback = typeof status !== 'number';
|
shouldFallback = !isNumber(status);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Request Failed at ${endpoint}: ${error.message}`);
|
logger.warn(`Request Failed at ${endpoint}: ${error.message}`);
|
||||||
lastError = error instanceof Error ? error : new Error(String(error));
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { isEmpty, isString } from 'lodash-es';
|
||||||
|
|
||||||
export type RequestHeaderValue = string | string[] | undefined;
|
export type RequestHeaderValue = string | string[] | undefined;
|
||||||
|
|
||||||
export type RequestHeaders = Record<string, RequestHeaderValue>;
|
export type RequestHeaders = Record<string, RequestHeaderValue>;
|
||||||
|
|
||||||
export function hasConfiguredApiKey(apiKey: string | undefined): apiKey is string {
|
export function hasConfiguredApiKey(apiKey: string | undefined): apiKey is string {
|
||||||
return typeof apiKey === 'string' && apiKey.trim() !== '';
|
return isString(apiKey) && !isEmpty(apiKey.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractApiKeyToken(headers: RequestHeaders): string | null {
|
export function extractApiKeyToken(headers: RequestHeaders): string | null {
|
||||||
@@ -29,16 +31,16 @@ export function extractApiKeyToken(headers: RequestHeaders): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readHeaderValue(headerValue: RequestHeaderValue): string | null {
|
function readHeaderValue(headerValue: RequestHeaderValue): string | null {
|
||||||
if (typeof headerValue === 'string') {
|
if (isString(headerValue)) {
|
||||||
const trimmedValue = headerValue.trim();
|
const trimmedValue = headerValue.trim();
|
||||||
return trimmedValue !== '' ? trimmedValue : null;
|
return !isEmpty(trimmedValue) ? trimmedValue : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(headerValue)) {
|
if (Array.isArray(headerValue)) {
|
||||||
for (const value of headerValue) {
|
for (const value of headerValue) {
|
||||||
if (typeof value === 'string') {
|
if (isString(value)) {
|
||||||
const trimmedValue = value.trim();
|
const trimmedValue = value.trim();
|
||||||
if (trimmedValue !== '') {
|
if (!isEmpty(trimmedValue)) {
|
||||||
return trimmedValue;
|
return trimmedValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import axios, { AxiosProxyConfig, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosProxyConfig, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import { isNil } from 'lodash-es';
|
import { isEmpty, isFunction, isNil, isObjectLike, isString } from 'lodash-es';
|
||||||
import { GeminiRequest, GeminiResponse } from '../interfaces/request-interfaces';
|
import { GeminiRequest, GeminiResponse } from '../interfaces/request-interfaces';
|
||||||
import { GeminiInternalRequest } from '../../../../lib/antigravity/types';
|
import { GeminiInternalRequest } from '../../../../lib/antigravity/types';
|
||||||
import { getServerConfig } from '../../../server-config';
|
import { getServerConfig } from '../../../server-config';
|
||||||
@@ -131,10 +131,10 @@ export class GeminiClient {
|
|||||||
extraHeaders,
|
extraHeaders,
|
||||||
);
|
);
|
||||||
const payload = response.data;
|
const payload = response.data;
|
||||||
if (payload && typeof payload === 'object' && 'response' in payload) {
|
if (isObjectLike(payload) && 'response' in payload) {
|
||||||
return payload.response;
|
return (payload as { response: GeminiResponse }).response;
|
||||||
}
|
}
|
||||||
return payload;
|
return payload as GeminiResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInternalBaseUrls(): string[] {
|
private getInternalBaseUrls(): string[] {
|
||||||
@@ -255,7 +255,7 @@ export class GeminiClient {
|
|||||||
return fromObject;
|
return fromObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof responseData === 'string') {
|
if (isString(responseData)) {
|
||||||
return this.extractAxiosErrorMessageFromText(responseData);
|
return this.extractAxiosErrorMessageFromText(responseData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,20 +272,20 @@ export class GeminiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extractAxiosErrorMessageFromObject(responseData: unknown): string | null {
|
private extractAxiosErrorMessageFromObject(responseData: unknown): string | null {
|
||||||
if (!responseData || typeof responseData !== 'object' || this.isReadableStream(responseData)) {
|
if (!isObjectLike(responseData) || this.isReadableStream(responseData)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorRecord = (responseData as { error?: unknown }).error;
|
const errorRecord = (responseData as { error?: unknown }).error;
|
||||||
if (errorRecord && typeof errorRecord === 'object') {
|
if (isObjectLike(errorRecord)) {
|
||||||
const message = (errorRecord as { message?: unknown }).message;
|
const message = (errorRecord as { message?: unknown }).message;
|
||||||
if (typeof message === 'string' && message.trim() !== '') {
|
if (isString(message) && !isEmpty(message.trim())) {
|
||||||
return message.trim();
|
return message.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = (responseData as { message?: unknown }).message;
|
const message = (responseData as { message?: unknown }).message;
|
||||||
if (typeof message === 'string' && message.trim() !== '') {
|
if (isString(message) && !isEmpty(message.trim())) {
|
||||||
return message.trim();
|
return message.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,11 +333,7 @@ export class GeminiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isReadableStream(value: unknown): value is NodeJS.ReadableStream {
|
private isReadableStream(value: unknown): value is NodeJS.ReadableStream {
|
||||||
return (
|
return isObjectLike(value) && isFunction((value as { pipe?: unknown }).pipe);
|
||||||
Boolean(value) &&
|
|
||||||
typeof value === 'object' &&
|
|
||||||
typeof (value as { pipe?: unknown }).pipe === 'function'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readStreamAsText(stream: NodeJS.ReadableStream): Promise<string | null> {
|
private async readStreamAsText(stream: NodeJS.ReadableStream): Promise<string | null> {
|
||||||
@@ -407,17 +403,17 @@ export class GeminiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extractRetryAfterHeader(headers: unknown): string | undefined {
|
private extractRetryAfterHeader(headers: unknown): string | undefined {
|
||||||
if (!headers || typeof headers !== 'object') {
|
if (!isObjectLike(headers)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const retryAfter = (headers as Record<string, unknown>)['retry-after'];
|
const retryAfter = (headers as Record<string, unknown>)['retry-after'];
|
||||||
if (typeof retryAfter === 'string' && retryAfter.trim() !== '') {
|
if (isString(retryAfter) && !isEmpty(retryAfter.trim())) {
|
||||||
return retryAfter.trim();
|
return retryAfter.trim();
|
||||||
}
|
}
|
||||||
if (Array.isArray(retryAfter) && retryAfter.length > 0) {
|
if (Array.isArray(retryAfter) && retryAfter.length > 0) {
|
||||||
const first = retryAfter[0];
|
const first = retryAfter[0];
|
||||||
if (typeof first === 'string' && first.trim() !== '') {
|
if (isString(first) && !isEmpty(first.trim())) {
|
||||||
return first.trim();
|
return first.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,7 +435,7 @@ export class GeminiClient {
|
|||||||
try {
|
try {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
return JSON.stringify(obj, (key, value) => {
|
return JSON.stringify(obj, (key, value) => {
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (isObjectLike(value)) {
|
||||||
if (seen.has(value)) return '[Circular]';
|
if (seen.has(value)) return '[Circular]';
|
||||||
seen.add(value);
|
seen.add(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isNumber, isObjectLike, isString } from 'lodash-es';
|
||||||
|
|
||||||
export interface UpstreamErrorHeaders {
|
export interface UpstreamErrorHeaders {
|
||||||
retryAfter?: string;
|
retryAfter?: string;
|
||||||
}
|
}
|
||||||
@@ -16,10 +18,10 @@ export class UpstreamRequestError extends Error {
|
|||||||
super(params.message);
|
super(params.message);
|
||||||
this.name = 'UpstreamRequestError';
|
this.name = 'UpstreamRequestError';
|
||||||
|
|
||||||
if (params.status && typeof params.status !== 'number') {
|
if (params.status && !isNumber(params.status)) {
|
||||||
throw new TypeError('status must be a number');
|
throw new TypeError('status must be a number');
|
||||||
}
|
}
|
||||||
if (params.headers && typeof params.headers !== 'object') {
|
if (params.headers && !isObjectLike(params.headers)) {
|
||||||
throw new TypeError('headers must be an object');
|
throw new TypeError('headers must be an object');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ export class UpstreamRequestError extends Error {
|
|||||||
this.headers = params.headers;
|
this.headers = params.headers;
|
||||||
|
|
||||||
// Sanitize and limit body size
|
// Sanitize and limit body size
|
||||||
if (typeof params.body === 'string') {
|
if (isString(params.body)) {
|
||||||
const sanitized = params.body.replace(/<[^>]*>?/gm, '');
|
const sanitized = params.body.replace(/<[^>]*>?/gm, '');
|
||||||
this.body = sanitized.substring(0, 1000);
|
this.body = sanitized.substring(0, 1000);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Optional,
|
Optional,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FastifyReply } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty, isFunction, isNumber, isString } from 'lodash-es';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { ProxyGuard } from './proxy.guard';
|
import { ProxyGuard } from './proxy.guard';
|
||||||
@@ -198,7 +198,7 @@ export class GeminiController {
|
|||||||
const candidates = (response.candidates ?? []).map((candidate, index) => ({
|
const candidates = (response.candidates ?? []).map((candidate, index) => ({
|
||||||
content: candidate.content,
|
content: candidate.content,
|
||||||
finishReason: candidate.finishReason,
|
finishReason: candidate.finishReason,
|
||||||
index: typeof candidate.index === 'number' ? candidate.index : index,
|
index: isNumber(candidate.index) ? candidate.index : index,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const normalized: GeminiResponse = {
|
const normalized: GeminiResponse = {
|
||||||
@@ -246,8 +246,8 @@ export class GeminiController {
|
|||||||
private writeObservableSseResponse(res: FastifyReply, stream: Observable<unknown>): void {
|
private writeObservableSseResponse(res: FastifyReply, stream: Observable<unknown>): void {
|
||||||
if (
|
if (
|
||||||
!res.raw ||
|
!res.raw ||
|
||||||
typeof res.raw.writeHead !== 'function' ||
|
!isFunction(res.raw.writeHead) ||
|
||||||
typeof res.raw.write !== 'function'
|
!isFunction(res.raw.write)
|
||||||
) {
|
) {
|
||||||
res.header('Content-Type', 'text/event-stream');
|
res.header('Content-Type', 'text/event-stream');
|
||||||
res.header('Cache-Control', 'no-cache');
|
res.header('Cache-Control', 'no-cache');
|
||||||
@@ -271,7 +271,7 @@ export class GeminiController {
|
|||||||
if (res.raw.writableEnded) {
|
if (res.raw.writableEnded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = typeof chunk === 'string' ? chunk : String(chunk ?? '');
|
const payload = isString(chunk) ? chunk : String(chunk ?? '');
|
||||||
res.raw.write(payload);
|
res.raw.write(payload);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@@ -302,6 +302,6 @@ export class GeminiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private supportsReplyHijack(reply: FastifyReply): reply is FastifyReply & { hijack: () => void } {
|
private supportsReplyHijack(reply: FastifyReply): reply is FastifyReply & { hijack: () => void } {
|
||||||
return typeof (reply as { hijack?: unknown }).hijack === 'function';
|
return isFunction((reply as { hijack?: unknown }).hijack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Optional,
|
Optional,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { isNil, isPlainObject, isString } from 'lodash-es';
|
import { isEmpty, isFunction, isNil, isObjectLike, isPlainObject, isString } from 'lodash-es';
|
||||||
import { ProxyService } from './proxy.service';
|
import { ProxyService } from './proxy.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -339,7 +339,7 @@ export class ProxyController {
|
|||||||
private toLegacyTextCompletionsResponse(response: OpenAIChatResponse): Record<string, unknown> {
|
private toLegacyTextCompletionsResponse(response: OpenAIChatResponse): Record<string, unknown> {
|
||||||
const choice = response.choices?.[0];
|
const choice = response.choices?.[0];
|
||||||
const content = choice?.message?.content;
|
const content = choice?.message?.content;
|
||||||
const text = typeof content === 'string' ? content : '';
|
const text = isString(content) ? content : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: response.id,
|
id: response.id,
|
||||||
@@ -397,7 +397,7 @@ export class ProxyController {
|
|||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
}): OpenAIChatRequest {
|
}): OpenAIChatRequest {
|
||||||
const messages: OpenAIChatRequest['messages'] = [];
|
const messages: OpenAIChatRequest['messages'] = [];
|
||||||
if (body.instructions && body.instructions.trim() !== '') {
|
if (isString(body.instructions) && !isEmpty(body.instructions.trim())) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: body.instructions,
|
content: body.instructions,
|
||||||
@@ -484,7 +484,7 @@ export class ProxyController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof body.input === 'string') {
|
} else if (isString(body.input)) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: body.input,
|
content: body.input,
|
||||||
@@ -716,18 +716,14 @@ export class ProxyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isObservableLike(value: unknown): value is Observable<unknown> {
|
private isObservableLike(value: unknown): value is Observable<unknown> {
|
||||||
return (
|
return isObjectLike(value) && isFunction((value as { subscribe?: unknown }).subscribe);
|
||||||
Boolean(value) &&
|
|
||||||
typeof value === 'object' &&
|
|
||||||
typeof (value as { subscribe?: unknown }).subscribe === 'function'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private writeSseResponse(res: FastifyReply, stream: Observable<unknown>): void {
|
private writeSseResponse(res: FastifyReply, stream: Observable<unknown>): void {
|
||||||
if (
|
if (
|
||||||
!res.raw ||
|
!res.raw ||
|
||||||
typeof res.raw.writeHead !== 'function' ||
|
!isFunction(res.raw.writeHead) ||
|
||||||
typeof res.raw.write !== 'function'
|
!isFunction(res.raw.write)
|
||||||
) {
|
) {
|
||||||
res.header('Content-Type', 'text/event-stream');
|
res.header('Content-Type', 'text/event-stream');
|
||||||
res.header('Cache-Control', 'no-cache');
|
res.header('Cache-Control', 'no-cache');
|
||||||
@@ -736,7 +732,7 @@ export class ProxyController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof (res as { hijack?: () => void }).hijack === 'function') {
|
if (isFunction((res as { hijack?: () => void }).hijack)) {
|
||||||
(res as { hijack: () => void }).hijack();
|
(res as { hijack: () => void }).hijack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +747,7 @@ export class ProxyController {
|
|||||||
if (res.raw.writableEnded) {
|
if (res.raw.writableEnded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = typeof chunk === 'string' ? chunk : String(chunk ?? '');
|
const payload = isString(chunk) ? chunk : String(chunk ?? '');
|
||||||
res.raw.write(payload);
|
res.raw.write(payload);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@@ -804,7 +800,7 @@ export class ProxyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = result.choices?.[0]?.message?.content;
|
const content = result.choices?.[0]?.message?.content;
|
||||||
const image = this.extractInlineBase64Image(typeof content === 'string' ? content : '');
|
const image = this.extractInlineBase64Image(isString(content) ? content : '');
|
||||||
if (!image) {
|
if (!image) {
|
||||||
this.logProxyEndpointError(
|
this.logProxyEndpointError(
|
||||||
'/v1/images/generations',
|
'/v1/images/generations',
|
||||||
@@ -908,7 +904,7 @@ export class ProxyController {
|
|||||||
|
|
||||||
if (!userMessage) {
|
if (!userMessage) {
|
||||||
parts.push({ text: fallbackPrompt || 'Please generate an image based on this request.' });
|
parts.push({ text: fallbackPrompt || 'Please generate an image based on this request.' });
|
||||||
} else if (typeof userMessage.content === 'string') {
|
} else if (isString(userMessage.content)) {
|
||||||
parts.push({
|
parts.push({
|
||||||
text:
|
text:
|
||||||
userMessage.content ||
|
userMessage.content ||
|
||||||
@@ -917,7 +913,7 @@ export class ProxyController {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
for (const block of userMessage.content) {
|
for (const block of userMessage.content) {
|
||||||
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim() !== '') {
|
if (block.type === 'text' && isString(block.text) && !isEmpty(block.text.trim())) {
|
||||||
textParts.push(block.text);
|
textParts.push(block.text);
|
||||||
}
|
}
|
||||||
if (block.type === 'image_url') {
|
if (block.type === 'image_url') {
|
||||||
@@ -964,7 +960,7 @@ export class ProxyController {
|
|||||||
|
|
||||||
private hasMultipartBoundary(req: FastifyRequest): boolean {
|
private hasMultipartBoundary(req: FastifyRequest): boolean {
|
||||||
const contentType = req.headers['content-type'];
|
const contentType = req.headers['content-type'];
|
||||||
if (typeof contentType !== 'string') {
|
if (!isString(contentType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||||
import { isEmpty, isNil, isPlainObject, isString } from 'lodash-es';
|
import { isEmpty, isNil, isNumber, isPlainObject, isString } from 'lodash-es';
|
||||||
import { TokenManagerService } from './token-manager.service';
|
import { TokenManagerService } from './token-manager.service';
|
||||||
import { GeminiClient } from './clients/gemini.client';
|
import { GeminiClient } from './clients/gemini.client';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@@ -511,7 +511,7 @@ export class ProxyService {
|
|||||||
private getModelOutputCap(accountId: string, model: string): number {
|
private getModelOutputCap(accountId: string, model: string): number {
|
||||||
const normalizedModel = this.normalizeModelIdentifier(model);
|
const normalizedModel = this.normalizeModelIdentifier(model);
|
||||||
const dynamicCap = this.tokenManager.getModelOutputLimitForAccount(accountId, normalizedModel);
|
const dynamicCap = this.tokenManager.getModelOutputLimitForAccount(accountId, normalizedModel);
|
||||||
if (typeof dynamicCap === 'number' && Number.isFinite(dynamicCap) && dynamicCap > 0) {
|
if (isNumber(dynamicCap) && Number.isFinite(dynamicCap) && dynamicCap > 0) {
|
||||||
return Math.floor(dynamicCap);
|
return Math.floor(dynamicCap);
|
||||||
}
|
}
|
||||||
return getMaxOutputTokens(normalizedModel);
|
return getMaxOutputTokens(normalizedModel);
|
||||||
@@ -523,7 +523,7 @@ export class ProxyService {
|
|||||||
accountId,
|
accountId,
|
||||||
normalizedModel,
|
normalizedModel,
|
||||||
);
|
);
|
||||||
if (typeof dynamicBudget === 'number' && Number.isFinite(dynamicBudget) && dynamicBudget >= 0) {
|
if (isNumber(dynamicBudget) && Number.isFinite(dynamicBudget) && dynamicBudget >= 0) {
|
||||||
return Math.floor(dynamicBudget);
|
return Math.floor(dynamicBudget);
|
||||||
}
|
}
|
||||||
return getThinkingBudget(normalizedModel);
|
return getThinkingBudget(normalizedModel);
|
||||||
@@ -548,12 +548,12 @@ export class ProxyService {
|
|||||||
| undefined;
|
| undefined;
|
||||||
const adaptiveSentinel =
|
const adaptiveSentinel =
|
||||||
thinkingConfig &&
|
thinkingConfig &&
|
||||||
(typeof thinkingConfig.thinkingLevel === 'string' ||
|
(isString(thinkingConfig.thinkingLevel) ||
|
||||||
thinkingConfig.thinkingBudget === -1 ||
|
thinkingConfig.thinkingBudget === -1 ||
|
||||||
thinkingConfig.thinkingBudget === 32768);
|
thinkingConfig.thinkingBudget === 32768);
|
||||||
|
|
||||||
if (thinkingConfig) {
|
if (thinkingConfig) {
|
||||||
if (!isClaudeModel && typeof thinkingConfig.thinkingLevel === 'string') {
|
if (!isClaudeModel && isString(thinkingConfig.thinkingLevel)) {
|
||||||
const converted = this.resolveThinkingLevelBudget(thinkingConfig.thinkingLevel);
|
const converted = this.resolveThinkingLevelBudget(thinkingConfig.thinkingLevel);
|
||||||
if (converted !== undefined) {
|
if (converted !== undefined) {
|
||||||
thinkingConfig.thinkingBudget = converted;
|
thinkingConfig.thinkingBudget = converted;
|
||||||
@@ -561,12 +561,12 @@ export class ProxyService {
|
|||||||
delete thinkingConfig.thinkingLevel;
|
delete thinkingConfig.thinkingLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof thinkingConfig.thinkingBudget === 'number' && thinkingConfig.thinkingBudget < 0) {
|
if (isNumber(thinkingConfig.thinkingBudget) && thinkingConfig.thinkingBudget < 0) {
|
||||||
thinkingConfig.thinkingBudget = Math.min(thinkingBudgetCap, 24576);
|
thinkingConfig.thinkingBudget = Math.min(thinkingBudgetCap, 24576);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof thinkingConfig.thinkingBudget === 'number' &&
|
isNumber(thinkingConfig.thinkingBudget) &&
|
||||||
Number.isFinite(thinkingConfig.thinkingBudget)
|
Number.isFinite(thinkingConfig.thinkingBudget)
|
||||||
) {
|
) {
|
||||||
thinkingConfig.thinkingBudget = Math.min(
|
thinkingConfig.thinkingBudget = Math.min(
|
||||||
@@ -595,7 +595,7 @@ export class ProxyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof generationConfig.maxOutputTokens === 'number' &&
|
isNumber(generationConfig.maxOutputTokens) &&
|
||||||
Number.isFinite(generationConfig.maxOutputTokens)
|
Number.isFinite(generationConfig.maxOutputTokens)
|
||||||
) {
|
) {
|
||||||
generationConfig.maxOutputTokens = Math.min(
|
generationConfig.maxOutputTokens = Math.min(
|
||||||
@@ -634,7 +634,7 @@ export class ProxyService {
|
|||||||
? response.candidates.map((candidate, index) => ({
|
? response.candidates.map((candidate, index) => ({
|
||||||
content: candidate?.content,
|
content: candidate?.content,
|
||||||
finishReason: candidate?.finishReason,
|
finishReason: candidate?.finishReason,
|
||||||
index: typeof candidate?.index === 'number' ? candidate.index : index,
|
index: isNumber(candidate?.index) ? candidate.index : index,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -1115,7 +1115,7 @@ export class ProxyService {
|
|||||||
const choice = response.choices?.[0];
|
const choice = response.choices?.[0];
|
||||||
const finishReason = choice?.finish_reason ?? 'stop';
|
const finishReason = choice?.finish_reason ?? 'stop';
|
||||||
const content =
|
const content =
|
||||||
choice?.message && typeof choice.message.content === 'string' ? choice.message.content : '';
|
choice?.message && isString(choice.message.content) ? choice.message.content : '';
|
||||||
const chunkSize = 80;
|
const chunkSize = 80;
|
||||||
|
|
||||||
if (content.length === 0) {
|
if (content.length === 0) {
|
||||||
@@ -1216,7 +1216,7 @@ export class ProxyService {
|
|||||||
systemInstruction: request.systemInstruction
|
systemInstruction: request.systemInstruction
|
||||||
? {
|
? {
|
||||||
parts: request.systemInstruction.parts
|
parts: request.systemInstruction.parts
|
||||||
.filter((part): part is { text: string } => typeof part.text === 'string')
|
.filter((part): part is { text: string } => isString(part.text))
|
||||||
.map((part) => ({ text: part.text })),
|
.map((part) => ({ text: part.text })),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -1294,7 +1294,7 @@ export class ProxyService {
|
|||||||
private convertOpenAIPartsToAnthropicContent(
|
private convertOpenAIPartsToAnthropicContent(
|
||||||
content: OpenAIChatRequest['messages'][number]['content'],
|
content: OpenAIChatRequest['messages'][number]['content'],
|
||||||
): AnthropicContent[] {
|
): AnthropicContent[] {
|
||||||
if (typeof content === 'string') {
|
if (isString(content)) {
|
||||||
return content.trim() ? [{ type: 'text', text: content }] : [];
|
return content.trim() ? [{ type: 'text', text: content }] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,7 +1328,7 @@ export class ProxyService {
|
|||||||
private extractOpenAITextContent(
|
private extractOpenAITextContent(
|
||||||
content: OpenAIChatRequest['messages'][number]['content'],
|
content: OpenAIChatRequest['messages'][number]['content'],
|
||||||
): string {
|
): string {
|
||||||
if (typeof content === 'string') {
|
if (isString(content)) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,14 +1339,14 @@ export class ProxyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseOpenAIFunctionArguments(argumentsString: string): Record<string, unknown> {
|
private parseOpenAIFunctionArguments(argumentsString: string): Record<string, unknown> {
|
||||||
if (!argumentsString || argumentsString.trim() === '') {
|
if (isEmpty(argumentsString.trim())) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(argumentsString);
|
const parsed = JSON.parse(argumentsString);
|
||||||
if (this.isRecord(parsed)) {
|
if (isPlainObject(parsed)) {
|
||||||
return parsed;
|
return parsed as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
return { value: parsed };
|
return { value: parsed };
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1446,7 +1446,7 @@ export class ProxyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private normalizeToolCallArguments(input: unknown): string {
|
private normalizeToolCallArguments(input: unknown): string {
|
||||||
if (typeof input === 'string') {
|
if (isString(input)) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
if (isNil(input)) {
|
if (isNil(input)) {
|
||||||
@@ -1730,7 +1730,7 @@ export class ProxyService {
|
|||||||
const metadata = request.metadata;
|
const metadata = request.metadata;
|
||||||
const sessionCandidate =
|
const sessionCandidate =
|
||||||
metadata?.session_id ?? metadata?.sessionId ?? metadata?.user_id ?? metadata?.userId;
|
metadata?.session_id ?? metadata?.sessionId ?? metadata?.user_id ?? metadata?.userId;
|
||||||
if (!isString(sessionCandidate) || sessionCandidate.trim() === '') {
|
if (!isString(sessionCandidate) || isEmpty(sessionCandidate.trim())) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return `anthropic:${sessionCandidate.trim()}`;
|
return `anthropic:${sessionCandidate.trim()}`;
|
||||||
@@ -1740,17 +1740,13 @@ export class ProxyService {
|
|||||||
const extra = request.extra;
|
const extra = request.extra;
|
||||||
const sessionCandidate =
|
const sessionCandidate =
|
||||||
extra?.session_id ?? extra?.sessionId ?? extra?.user_id ?? extra?.userId;
|
extra?.session_id ?? extra?.sessionId ?? extra?.user_id ?? extra?.userId;
|
||||||
if (!isString(sessionCandidate) || sessionCandidate.trim() === '') {
|
if (!isString(sessionCandidate) || isEmpty(sessionCandidate.trim())) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return `openai:${sessionCandidate.trim()}`;
|
return `openai:${sessionCandidate.trim()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isRecord(value: unknown): value is Record<string, unknown> {
|
private isGeminiPart(value: unknown): value is InternalGeminiPart {
|
||||||
return isPlainObject(value);
|
return isPlainObject(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isGeminiPart(value: unknown): value is InternalGeminiPart {
|
|
||||||
return this.isRecord(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isEmpty, isNumber, isObjectLike, isString } from 'lodash-es';
|
||||||
|
|
||||||
export enum RateLimitReason {
|
export enum RateLimitReason {
|
||||||
QuotaExhausted = 'quota_exhausted',
|
QuotaExhausted = 'quota_exhausted',
|
||||||
RateLimitExceeded = 'rate_limit_exceeded',
|
RateLimitExceeded = 'rate_limit_exceeded',
|
||||||
@@ -68,7 +70,7 @@ function tryParseGoogleErrorBody(body: string | undefined): ParsedGoogleErrorBod
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed) as ParsedGoogleErrorBody;
|
const parsed = JSON.parse(trimmed) as ParsedGoogleErrorBody;
|
||||||
if (!parsed || typeof parsed !== 'object') {
|
if (!isObjectLike(parsed)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
@@ -96,7 +98,7 @@ export class RateLimitTracker {
|
|||||||
private readonly failureCounts = new Map<string, FailureCountEntry>();
|
private readonly failureCounts = new Map<string, FailureCountEntry>();
|
||||||
|
|
||||||
private buildLockoutKey(accountId: string, model?: string): string {
|
private buildLockoutKey(accountId: string, model?: string): string {
|
||||||
if (model && model.trim() !== '') {
|
if (!isEmpty(model?.trim() ?? '')) {
|
||||||
return `${accountId}:${model}`;
|
return `${accountId}:${model}`;
|
||||||
}
|
}
|
||||||
return accountId;
|
return accountId;
|
||||||
@@ -150,7 +152,7 @@ export class RateLimitTracker {
|
|||||||
model?: string,
|
model?: string,
|
||||||
): void {
|
): void {
|
||||||
const retryAfterSec = Math.max(2, Math.ceil((resetTimeMs - Date.now()) / 1000));
|
const retryAfterSec = Math.max(2, Math.ceil((resetTimeMs - Date.now()) / 1000));
|
||||||
const key = model && model.trim() !== '' ? this.buildLockoutKey(accountId, model) : accountId;
|
const key = !isEmpty(model?.trim() ?? '') ? this.buildLockoutKey(accountId, model) : accountId;
|
||||||
this.lockoutByKey.set(key, {
|
this.lockoutByKey.set(key, {
|
||||||
resetTimeMs,
|
resetTimeMs,
|
||||||
retryAfterSec,
|
retryAfterSec,
|
||||||
@@ -240,7 +242,7 @@ export class RateLimitTracker {
|
|||||||
const parsedBody = tryParseGoogleErrorBody(body);
|
const parsedBody = tryParseGoogleErrorBody(body);
|
||||||
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody?.error?.details : [];
|
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody?.error?.details : [];
|
||||||
for (const detail of details) {
|
for (const detail of details) {
|
||||||
if (typeof detail.reason !== 'string' || detail.reason.trim() === '') {
|
if (!isString(detail.reason) || isEmpty(detail.reason.trim())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mappedReason = mapGoogleReasonToTrackerReason(detail.reason);
|
const mappedReason = mapGoogleReasonToTrackerReason(detail.reason);
|
||||||
@@ -354,7 +356,7 @@ export class RateLimitTracker {
|
|||||||
const parsedBody = tryParseGoogleErrorBody(body);
|
const parsedBody = tryParseGoogleErrorBody(body);
|
||||||
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody.error.details : [];
|
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody.error.details : [];
|
||||||
for (const detail of details) {
|
for (const detail of details) {
|
||||||
if (typeof detail.retryDelay === 'string' && detail.retryDelay.trim() !== '') {
|
if (isString(detail.retryDelay) && !isEmpty(detail.retryDelay.trim())) {
|
||||||
const parsedDelay = parseDurationToSeconds(detail.retryDelay);
|
const parsedDelay = parseDurationToSeconds(detail.retryDelay);
|
||||||
if (parsedDelay !== null) {
|
if (parsedDelay !== null) {
|
||||||
return parsedDelay;
|
return parsedDelay;
|
||||||
@@ -362,8 +364,8 @@ export class RateLimitTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof detail.metadata?.retryDelay === 'string' &&
|
isString(detail.metadata?.retryDelay) &&
|
||||||
detail.metadata.retryDelay.trim() !== ''
|
!isEmpty(detail.metadata.retryDelay.trim())
|
||||||
) {
|
) {
|
||||||
const parsedDelay = parseDurationToSeconds(detail.metadata.retryDelay);
|
const parsedDelay = parseDurationToSeconds(detail.metadata.retryDelay);
|
||||||
if (parsedDelay !== null) {
|
if (parsedDelay !== null) {
|
||||||
@@ -372,8 +374,8 @@ export class RateLimitTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof detail.metadata?.quotaResetDelay === 'string' &&
|
isString(detail.metadata?.quotaResetDelay) &&
|
||||||
detail.metadata.quotaResetDelay.trim() !== ''
|
!isEmpty(detail.metadata.quotaResetDelay.trim())
|
||||||
) {
|
) {
|
||||||
const parsedDelay = parseDurationToSeconds(detail.metadata.quotaResetDelay);
|
const parsedDelay = parseDurationToSeconds(detail.metadata.quotaResetDelay);
|
||||||
if (parsedDelay !== null) {
|
if (parsedDelay !== null) {
|
||||||
@@ -383,7 +385,7 @@ export class RateLimitTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retryAfter = parsedBody?.error?.retry_after;
|
const retryAfter = parsedBody?.error?.retry_after;
|
||||||
if (typeof retryAfter === 'number' && retryAfter > 0) {
|
if (isNumber(retryAfter) && retryAfter > 0) {
|
||||||
return Math.ceil(retryAfter);
|
return Math.ceil(retryAfter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { isString } from 'lodash-es';
|
||||||
import { getAntigravityVersion } from '../../../utils/antigravityVersion';
|
import { getAntigravityVersion } from '../../../utils/antigravityVersion';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ let cachedUserAgentResolution: UserAgentResolution | null = null;
|
|||||||
let pendingUserAgentResolution: Promise<UserAgentResolution> | null = null;
|
let pendingUserAgentResolution: Promise<UserAgentResolution> | null = null;
|
||||||
|
|
||||||
function normalizeNonEmptyString(value: string | null | undefined): string | null {
|
function normalizeNonEmptyString(value: string | null | undefined): string | null {
|
||||||
if (typeof value !== 'string') {
|
if (!isString(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ async function fetchTextPayload(url: string): Promise<string | null> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof response.data === 'string') {
|
if (isString(response.data)) {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { isEmpty, isNumber, isString } from 'lodash-es';
|
||||||
import { CloudAccountRepo } from '../../../ipc/database/cloudHandler';
|
import { CloudAccountRepo } from '../../../ipc/database/cloudHandler';
|
||||||
import { CloudAccount, CloudQuotaData } from '../../../types/cloudAccount';
|
import { CloudAccount, CloudQuotaData } from '../../../types/cloudAccount';
|
||||||
import { GoogleAPIService } from '../../../services/GoogleAPIService';
|
import { GoogleAPIService } from '../../../services/GoogleAPIService';
|
||||||
@@ -36,7 +37,7 @@ interface GetNextTokenOptions {
|
|||||||
type TokenEntry = [string, TokenData];
|
type TokenEntry = [string, TokenData];
|
||||||
|
|
||||||
function normalizeProjectId(projectId: string | null | undefined): string | undefined {
|
function normalizeProjectId(projectId: string | null | undefined): string | undefined {
|
||||||
if (typeof projectId !== 'string') {
|
if (!isString(projectId)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ function normalizeProjectId(projectId: string | null | undefined): string | unde
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeModelId(modelId: string | null | undefined): string | undefined {
|
function normalizeModelId(modelId: string | null | undefined): string | undefined {
|
||||||
if (typeof modelId !== 'string') {
|
if (!isString(modelId)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const normalized = modelId.replace(/^models\//i, '').trim();
|
const normalized = modelId.replace(/^models\//i, '').trim();
|
||||||
@@ -61,7 +62,7 @@ function normalizeModelId(modelId: string | null | undefined): string | undefine
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeClientKey(clientKey: string | undefined): string | undefined {
|
function normalizeClientKey(clientKey: string | undefined): string | undefined {
|
||||||
if (typeof clientKey !== 'string') {
|
if (!isString(clientKey)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const normalized = clientKey.trim().toLowerCase();
|
const normalized = clientKey.trim().toLowerCase();
|
||||||
@@ -190,7 +191,7 @@ export class TokenManagerService implements OnModuleInit {
|
|||||||
const accountId = this.resolveAccountId(params.accountIdOrEmail) ?? params.accountIdOrEmail;
|
const accountId = this.resolveAccountId(params.accountIdOrEmail) ?? params.accountIdOrEmail;
|
||||||
const normalizedModel = normalizeModelId(params.model);
|
const normalizedModel = normalizeModelId(params.model);
|
||||||
const hasExplicitRetryWindow =
|
const hasExplicitRetryWindow =
|
||||||
Boolean(params.retryAfter && params.retryAfter.trim() !== '') ||
|
Boolean(isString(params.retryAfter) && !isEmpty(params.retryAfter.trim())) ||
|
||||||
Boolean(params.body && params.body.includes('quotaResetDelay'));
|
Boolean(params.body && params.body.includes('quotaResetDelay'));
|
||||||
|
|
||||||
if (!hasExplicitRetryWindow && (params.status ?? 0) === 429) {
|
if (!hasExplicitRetryWindow && (params.status ?? 0) === 429) {
|
||||||
@@ -215,7 +216,7 @@ export class TokenManagerService implements OnModuleInit {
|
|||||||
if (
|
if (
|
||||||
parsed.reason !== RateLimitReason.QuotaExhausted ||
|
parsed.reason !== RateLimitReason.QuotaExhausted ||
|
||||||
!parsed.model ||
|
!parsed.model ||
|
||||||
parsed.model.trim() === ''
|
isEmpty(parsed.model.trim())
|
||||||
) {
|
) {
|
||||||
this.accountCooldowns.set(accountId, Date.now() + parsed.retryAfterSec * 1000);
|
this.accountCooldowns.set(accountId, Date.now() + parsed.retryAfterSec * 1000);
|
||||||
}
|
}
|
||||||
@@ -258,7 +259,7 @@ export class TokenManagerService implements OnModuleInit {
|
|||||||
if (
|
if (
|
||||||
parsed.reason !== RateLimitReason.QuotaExhausted ||
|
parsed.reason !== RateLimitReason.QuotaExhausted ||
|
||||||
!parsed.model ||
|
!parsed.model ||
|
||||||
parsed.model.trim() === ''
|
isEmpty(parsed.model.trim())
|
||||||
) {
|
) {
|
||||||
this.accountCooldowns.set(accountId, Date.now() + parsed.retryAfterSec * 1000);
|
this.accountCooldowns.set(accountId, Date.now() + parsed.retryAfterSec * 1000);
|
||||||
}
|
}
|
||||||
@@ -661,14 +662,14 @@ export class TokenManagerService implements OnModuleInit {
|
|||||||
|
|
||||||
const limitCandidate = modelInfo.max_output_tokens ?? modelInfo.max_tokens;
|
const limitCandidate = modelInfo.max_output_tokens ?? modelInfo.max_tokens;
|
||||||
if (
|
if (
|
||||||
typeof limitCandidate === 'number' &&
|
isNumber(limitCandidate) &&
|
||||||
Number.isFinite(limitCandidate) &&
|
Number.isFinite(limitCandidate) &&
|
||||||
limitCandidate > 0
|
limitCandidate > 0
|
||||||
) {
|
) {
|
||||||
modelLimits[normalizedModel] = Math.floor(limitCandidate);
|
modelLimits[normalizedModel] = Math.floor(limitCandidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof modelInfo.resetTime === 'string' && modelInfo.resetTime.trim() !== '') {
|
if (isString(modelInfo.resetTime) && !isEmpty(modelInfo.resetTime.trim())) {
|
||||||
modelResetTimes[normalizedModel] = modelInfo.resetTime;
|
modelResetTimes[normalizedModel] = modelInfo.resetTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -692,7 +693,7 @@ export class TokenManagerService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private findEarliestQuotaResetTime(modelResetTimes: Record<string, string>): string | null {
|
private findEarliestQuotaResetTime(modelResetTimes: Record<string, string>): string | null {
|
||||||
const validTimes = Object.values(modelResetTimes).filter((value) => value.trim() !== '');
|
const validTimes = Object.values(modelResetTimes).filter((value) => !isEmpty(value.trim()));
|
||||||
if (validTimes.length === 0) {
|
if (validTimes.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1084,7 +1085,7 @@ export class TokenManagerService implements OnModuleInit {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const budget = modelInfo?.thinking_budget;
|
const budget = modelInfo?.thinking_budget;
|
||||||
if (typeof budget === 'number' && Number.isFinite(budget) && budget >= 0) {
|
if (isNumber(budget) && Number.isFinite(budget) && budget >= 0) {
|
||||||
return Math.floor(budget);
|
return Math.floor(budget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
FALLBACK_VERSION,
|
FALLBACK_VERSION,
|
||||||
resolveLocalInstalledVersion,
|
resolveLocalInstalledVersion,
|
||||||
} from '@/server/modules/proxy/request-user-agent';
|
} from '@/server/modules/proxy/request-user-agent';
|
||||||
import { isNumber, isString } from 'lodash-es';
|
import { isEmpty, isNumber, isString, isUndefined } from 'lodash-es';
|
||||||
|
|
||||||
// --- Constants & Config ---
|
// --- Constants & Config ---
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ function buildOAuthClientRegistry(): OAuthClientRegistry {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const rawExtraClients = process.env[OAUTH_CLIENTS_ENV];
|
const rawExtraClients = process.env[OAUTH_CLIENTS_ENV];
|
||||||
if (isString(rawExtraClients) && rawExtraClients.trim() !== '') {
|
if (isString(rawExtraClients) && !isEmpty(rawExtraClients.trim())) {
|
||||||
for (const entry of rawExtraClients.split(';')) {
|
for (const entry of rawExtraClients.split(';')) {
|
||||||
const trimmed = entry.trim();
|
const trimmed = entry.trim();
|
||||||
if (trimmed === '') {
|
if (trimmed === '') {
|
||||||
@@ -180,7 +180,7 @@ function selectAuthClient(clientKey?: string): OAuthClientConfig {
|
|||||||
throw new Error('No OAuth clients configured');
|
throw new Error('No OAuth clients configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientKey && clientKey.trim() !== '') {
|
if (isString(clientKey) && !isEmpty(clientKey.trim())) {
|
||||||
const selected = getClientByKey(registry.clients, clientKey);
|
const selected = getClientByKey(registry.clients, clientKey);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
throw new Error(`Unknown OAuth client key: ${clientKey}`);
|
throw new Error(`Unknown OAuth client key: ${clientKey}`);
|
||||||
@@ -345,20 +345,20 @@ function buildInternalApiHeaders(accessToken: string): Record<string, string> {
|
|||||||
|
|
||||||
function resolveSubscriptionTier(payload: LoadProjectResponse): string | undefined {
|
function resolveSubscriptionTier(payload: LoadProjectResponse): string | undefined {
|
||||||
const paidTier = payload.paidTier;
|
const paidTier = payload.paidTier;
|
||||||
if (paidTier?.name && paidTier.name.trim() !== '') {
|
if (isString(paidTier?.name) && !isEmpty(paidTier.name.trim())) {
|
||||||
return paidTier.name;
|
return paidTier.name;
|
||||||
}
|
}
|
||||||
if (paidTier?.id && paidTier.id.trim() !== '') {
|
if (isString(paidTier?.id) && !isEmpty(paidTier.id.trim())) {
|
||||||
return paidTier.id;
|
return paidTier.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ineligible = Array.isArray(payload.ineligibleTiers) && payload.ineligibleTiers.length > 0;
|
const ineligible = Array.isArray(payload.ineligibleTiers) && payload.ineligibleTiers.length > 0;
|
||||||
if (!ineligible) {
|
if (!ineligible) {
|
||||||
const currentTier = payload.currentTier;
|
const currentTier = payload.currentTier;
|
||||||
if (currentTier?.name && currentTier.name.trim() !== '') {
|
if (isString(currentTier?.name) && !isEmpty(currentTier.name.trim())) {
|
||||||
return currentTier.name;
|
return currentTier.name;
|
||||||
}
|
}
|
||||||
if (currentTier?.id && currentTier.id.trim() !== '') {
|
if (isString(currentTier?.id) && !isEmpty(currentTier.id.trim())) {
|
||||||
return currentTier.id;
|
return currentTier.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,10 +366,10 @@ function resolveSubscriptionTier(payload: LoadProjectResponse): string | undefin
|
|||||||
if (Array.isArray(payload.allowedTiers)) {
|
if (Array.isArray(payload.allowedTiers)) {
|
||||||
const preferredAllowedTier =
|
const preferredAllowedTier =
|
||||||
payload.allowedTiers.find((tier) => tier.is_default === true) ?? payload.allowedTiers[0];
|
payload.allowedTiers.find((tier) => tier.is_default === true) ?? payload.allowedTiers[0];
|
||||||
if (preferredAllowedTier?.name && preferredAllowedTier.name.trim() !== '') {
|
if (isString(preferredAllowedTier?.name) && !isEmpty(preferredAllowedTier.name.trim())) {
|
||||||
return ineligible ? `${preferredAllowedTier.name} (Restricted)` : preferredAllowedTier.name;
|
return ineligible ? `${preferredAllowedTier.name} (Restricted)` : preferredAllowedTier.name;
|
||||||
}
|
}
|
||||||
if (preferredAllowedTier?.id && preferredAllowedTier.id.trim() !== '') {
|
if (isString(preferredAllowedTier?.id) && !isEmpty(preferredAllowedTier.id.trim())) {
|
||||||
return ineligible ? `${preferredAllowedTier.id} (Restricted)` : preferredAllowedTier.id;
|
return ineligible ? `${preferredAllowedTier.id} (Restricted)` : preferredAllowedTier.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,7 +410,7 @@ function toModelForwardingRules(
|
|||||||
|
|
||||||
const forwardingRules: Record<string, string> = {};
|
const forwardingRules: Record<string, string> = {};
|
||||||
for (const [oldModelId, deprecatedInfo] of Object.entries(deprecatedModelIds)) {
|
for (const [oldModelId, deprecatedInfo] of Object.entries(deprecatedModelIds)) {
|
||||||
if (typeof deprecatedInfo.newModelId === 'string' && deprecatedInfo.newModelId !== '') {
|
if (isString(deprecatedInfo.newModelId) && deprecatedInfo.newModelId !== '') {
|
||||||
forwardingRules[oldModelId] = deprecatedInfo.newModelId;
|
forwardingRules[oldModelId] = deprecatedInfo.newModelId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,7 +423,7 @@ function parseCreditAmount(value: string | number | undefined): number {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (isString(value)) {
|
||||||
const parsed = Number.parseInt(value, 10);
|
const parsed = Number.parseInt(value, 10);
|
||||||
if (Number.isFinite(parsed)) {
|
if (Number.isFinite(parsed)) {
|
||||||
return parsed;
|
return parsed;
|
||||||
@@ -448,7 +448,7 @@ function toAiCredits(
|
|||||||
? payload.remainingCredits
|
? payload.remainingCredits
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (typeof creditsValue === 'undefined') {
|
if (isUndefined(creditsValue)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,10 +517,10 @@ export class GoogleAPIService {
|
|||||||
): string | undefined {
|
): string | undefined {
|
||||||
const resolved = refreshedClientKey ?? currentToken.oauth_client_key;
|
const resolved = refreshedClientKey ?? currentToken.oauth_client_key;
|
||||||
const projectMissing =
|
const projectMissing =
|
||||||
typeof currentToken.project_id !== 'string' || currentToken.project_id.trim() === '';
|
!isString(currentToken.project_id) || isEmpty(currentToken.project_id.trim());
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof currentToken.oauth_client_key !== 'string' &&
|
!isString(currentToken.oauth_client_key) &&
|
||||||
projectMissing &&
|
projectMissing &&
|
||||||
resolved &&
|
resolved &&
|
||||||
normalizeClientKey(resolved) === DEFAULT_OAUTH_CLIENT_KEY
|
normalizeClientKey(resolved) === DEFAULT_OAUTH_CLIENT_KEY
|
||||||
@@ -812,7 +812,7 @@ export class GoogleAPIService {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = (await response.json()) as LoadProjectResponse;
|
const data = (await response.json()) as LoadProjectResponse;
|
||||||
if (typeof data.cloudaicompanionProject === 'string') {
|
if (isString(data.cloudaicompanionProject)) {
|
||||||
projectId = data.cloudaicompanionProject;
|
projectId = data.cloudaicompanionProject;
|
||||||
}
|
}
|
||||||
subscriptionTier = resolveSubscriptionTier(data);
|
subscriptionTier = resolveSubscriptionTier(data);
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ const injectCloudAccountsFailureScript = `
|
|||||||
(() => {
|
(() => {
|
||||||
const START_ORPC_SERVER = 'start-orpc-server';
|
const START_ORPC_SERVER = 'start-orpc-server';
|
||||||
const originalPostMessage = window.postMessage.bind(window);
|
const originalPostMessage = window.postMessage.bind(window);
|
||||||
|
const isStringValue = (value) => Object.prototype.toString.call(value) === '[object String]';
|
||||||
|
|
||||||
window.postMessage = function (message, targetOrigin, transfer) {
|
window.postMessage = function (message, targetOrigin, transfer) {
|
||||||
if (message === START_ORPC_SERVER && Array.isArray(transfer) && transfer[0]) {
|
if (message === START_ORPC_SERVER && Array.isArray(transfer) && transfer[0]) {
|
||||||
const serverPort = transfer[0];
|
const serverPort = transfer[0];
|
||||||
if (typeof serverPort.start === 'function') {
|
if (serverPort.start instanceof Function) {
|
||||||
serverPort.start();
|
serverPort.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
serverPort.onmessage = (event) => {
|
serverPort.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const request = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
const request = isStringValue(event.data) ? JSON.parse(event.data) : event.data;
|
||||||
const requestId = request && (request.i || request.id);
|
const requestId = request && (request.i || request.id);
|
||||||
const requestUrl = (request && request.p && request.p.u) || '';
|
const requestUrl = (request && request.p && request.p.u) || '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import { isEmpty, isString } from 'lodash-es';
|
||||||
|
|
||||||
import { transformClaudeRequestIn } from '../../lib/antigravity/ClaudeRequestMapper';
|
import { transformClaudeRequestIn } from '../../lib/antigravity/ClaudeRequestMapper';
|
||||||
import { ProxyService } from '../../server/modules/proxy/proxy.service';
|
import { ProxyService } from '../../server/modules/proxy/proxy.service';
|
||||||
@@ -34,7 +35,7 @@ class TestableProxyService extends ProxyService {
|
|||||||
|
|
||||||
function getEnv(name: string, fallback?: string): string {
|
function getEnv(name: string, fallback?: string): string {
|
||||||
const value = process.env[name];
|
const value = process.env[name];
|
||||||
if (value && value.trim() !== '') {
|
if (isString(value) && !isEmpty(value.trim())) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (fallback !== undefined) {
|
if (fallback !== undefined) {
|
||||||
@@ -46,7 +47,7 @@ function getEnv(name: string, fallback?: string): string {
|
|||||||
function getEnvFromList(names: string[], fallback?: string): string {
|
function getEnvFromList(names: string[], fallback?: string): string {
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
const value = process.env[name];
|
const value = process.env[name];
|
||||||
if (value && value.trim() !== '') {
|
if (isString(value) && !isEmpty(value.trim())) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +61,7 @@ function getLiveHeaders(apiKey?: string): Record<string, string> {
|
|||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
if (apiKey && apiKey.trim() !== '') {
|
if (isString(apiKey) && !isEmpty(apiKey.trim())) {
|
||||||
headers.Authorization = apiKey.startsWith('Bearer ') ? apiKey : `Bearer ${apiKey}`;
|
headers.Authorization = apiKey.startsWith('Bearer ') ? apiKey : `Bearer ${apiKey}`;
|
||||||
}
|
}
|
||||||
return headers;
|
return headers;
|
||||||
@@ -271,7 +272,7 @@ async function validateRuntimeAnthropicRequestFromRealTokenManager(): Promise<vo
|
|||||||
const captured = capturedBody as Record<string, unknown>;
|
const captured = capturedBody as Record<string, unknown>;
|
||||||
const tokenProjectId = selectedToken?.token?.project_id;
|
const tokenProjectId = selectedToken?.token?.project_id;
|
||||||
const normalizedTokenProjectId =
|
const normalizedTokenProjectId =
|
||||||
typeof tokenProjectId === 'string' && tokenProjectId.trim() !== ''
|
isString(tokenProjectId) && !isEmpty(tokenProjectId.trim())
|
||||||
? tokenProjectId.trim()
|
? tokenProjectId.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { isString } from 'lodash-es';
|
||||||
|
|
||||||
// Mock module to test timeout behavior
|
// Mock module to test timeout behavior
|
||||||
describe('GoogleAPIService Timeout', () => {
|
describe('GoogleAPIService Timeout', () => {
|
||||||
@@ -92,13 +93,13 @@ describe('GoogleAPIService OAuth clients', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (typeof originalOauthClientsEnv === 'string') {
|
if (isString(originalOauthClientsEnv)) {
|
||||||
process.env.ANTIGRAVITY_OAUTH_CLIENTS = originalOauthClientsEnv;
|
process.env.ANTIGRAVITY_OAUTH_CLIENTS = originalOauthClientsEnv;
|
||||||
} else {
|
} else {
|
||||||
delete process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
delete process.env.ANTIGRAVITY_OAUTH_CLIENTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof originalActiveOauthClientEnv === 'string') {
|
if (isString(originalActiveOauthClientEnv)) {
|
||||||
process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY = originalActiveOauthClientEnv;
|
process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY = originalActiveOauthClientEnv;
|
||||||
} else {
|
} else {
|
||||||
delete process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
delete process.env.ANTIGRAVITY_OAUTH_CLIENT_KEY;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import * as matchers from '@testing-library/jest-dom/matchers';
|
import * as matchers from '@testing-library/jest-dom/matchers';
|
||||||
|
import { isUndefined } from 'lodash-es';
|
||||||
import { expect, vi } from 'vitest';
|
import { expect, vi } from 'vitest';
|
||||||
|
|
||||||
expect.extend(matchers);
|
expect.extend(matchers);
|
||||||
|
|
||||||
// Mock window and localStorage for Node environment
|
// Mock window and localStorage for Node environment
|
||||||
if (typeof window === 'undefined') {
|
if (isUndefined((globalThis as { window?: unknown }).window)) {
|
||||||
global.window = {
|
global.window = {
|
||||||
matchMedia: vi.fn().mockImplementation((query) => ({
|
matchMedia: vi.fn().mockImplementation((query) => ({
|
||||||
matches: false,
|
matches: false,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isString } from 'lodash-es';
|
||||||
|
|
||||||
export const RATE_LIMIT_HINTS = [
|
export const RATE_LIMIT_HINTS = [
|
||||||
'resource_exhausted',
|
'resource_exhausted',
|
||||||
'too many requests',
|
'too many requests',
|
||||||
@@ -40,7 +42,7 @@ export function extractErrorMessage(error: unknown): string {
|
|||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return error.message;
|
return error.message;
|
||||||
}
|
}
|
||||||
if (typeof error === 'string') {
|
if (isString(error)) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
return String(error);
|
return String(error);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
|
import { isObjectLike } from 'lodash-es';
|
||||||
|
|
||||||
const KEYCHAIN_ERROR_CODE = 'ERR_KEYCHAIN_UNAVAILABLE';
|
const KEYCHAIN_ERROR_CODE = 'ERR_KEYCHAIN_UNAVAILABLE';
|
||||||
const KEYCHAIN_HINT_TRANSLOCATION = 'HINT_APP_TRANSLOCATION';
|
const KEYCHAIN_HINT_TRANSLOCATION = 'HINT_APP_TRANSLOCATION';
|
||||||
@@ -60,7 +61,7 @@ export function getLocalizedErrorMessage(error: unknown, t: TFunction): string {
|
|||||||
return rawMessage;
|
return rawMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && typeof error === 'object' && 'message' in error) {
|
if (isObjectLike(error)) {
|
||||||
const rawMessage = String((error as { message?: unknown }).message ?? '');
|
const rawMessage = String((error as { message?: unknown }).message ?? '');
|
||||||
const [code, hint] = rawMessage.split('|');
|
const [code, hint] = rawMessage.split('|');
|
||||||
if (code === KEYCHAIN_ERROR_CODE) {
|
if (code === KEYCHAIN_ERROR_CODE) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
|
||||||
export function shouldAutoSubmitGoogleAuthCode(params: {
|
export function shouldAutoSubmitGoogleAuthCode(params: {
|
||||||
authCode: string;
|
authCode: string;
|
||||||
isAddDialogOpen: boolean;
|
isAddDialogOpen: boolean;
|
||||||
@@ -5,7 +7,7 @@ export function shouldAutoSubmitGoogleAuthCode(params: {
|
|||||||
lastSubmittedAuthCode: string | null;
|
lastSubmittedAuthCode: string | null;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
return (
|
return (
|
||||||
params.authCode.trim() !== '' &&
|
!isEmpty(params.authCode.trim()) &&
|
||||||
params.isAddDialogOpen &&
|
params.isAddDialogOpen &&
|
||||||
!params.isPending &&
|
!params.isPending &&
|
||||||
params.authCode !== params.lastSubmittedAuthCode
|
params.authCode !== params.lastSubmittedAuthCode
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { isObjectLike } from 'lodash-es';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||||
import { getAgentDir } from './paths';
|
import { getAgentDir } from './paths';
|
||||||
@@ -42,7 +43,7 @@ function safeStringify(obj: unknown): string {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Handle circular references
|
// Handle circular references
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (isObjectLike(value)) {
|
||||||
if (seen.has(value)) {
|
if (seen.has(value)) {
|
||||||
return '[Circular]';
|
return '[Circular]';
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ class Logger {
|
|||||||
|
|
||||||
private formatArgs(args: unknown[]): string {
|
private formatArgs(args: unknown[]): string {
|
||||||
return args
|
return args
|
||||||
.map((arg) => (typeof arg === 'object' ? safeStringify(arg) : String(arg)))
|
.map((arg) => (isObjectLike(arg) ? safeStringify(arg) : String(arg)))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isObjectLike, isString } from 'lodash-es';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keys to mask in logs to avoid exposing sensitive data.
|
* Keys to mask in logs to avoid exposing sensitive data.
|
||||||
*/
|
*/
|
||||||
@@ -39,10 +41,10 @@ function sanitizeWithSeen(obj: unknown, seen: WeakSet<object>): unknown {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj === 'string') {
|
if (isString(obj)) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(obj);
|
const parsed = JSON.parse(obj);
|
||||||
if (typeof parsed === 'object' && parsed !== null) {
|
if (isObjectLike(parsed)) {
|
||||||
return JSON.stringify(sanitizeWithSeen(parsed, seen));
|
return JSON.stringify(sanitizeWithSeen(parsed, seen));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -59,7 +61,7 @@ function sanitizeWithSeen(obj: unknown, seen: WeakSet<object>): unknown {
|
|||||||
return obj.map((item) => sanitizeWithSeen(item, seen));
|
return obj.map((item) => sanitizeWithSeen(item, seen));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj === 'object') {
|
if (isObjectLike(obj)) {
|
||||||
if (seen.has(obj)) {
|
if (seen.has(obj)) {
|
||||||
return CIRCULAR_PLACEHOLDER;
|
return CIRCULAR_PLACEHOLDER;
|
||||||
}
|
}
|
||||||
@@ -94,7 +96,7 @@ export function safeStringifyPacket(obj: unknown): string {
|
|||||||
const sanitized = sanitizeObject(obj);
|
const sanitized = sanitizeObject(obj);
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
return JSON.stringify(sanitized, (key, value) => {
|
return JSON.stringify(sanitized, (key, value) => {
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (isObjectLike(value)) {
|
||||||
if (seen.has(value)) {
|
if (seen.has(value)) {
|
||||||
return '[Circular]';
|
return '[Circular]';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user