Browse Source

Merge branch 'main' into bugfix/positive-performance-from-all-time-high-in-watchlist

pull/8012/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
3af8d15366
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 33
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 170
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-with-market-price-between-chart-dates.spec.ts
  4. 14
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  5. 3
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  6. 3
      apps/client/src/app/pages/public/public-page.component.ts
  7. 8
      libs/common/src/lib/dtos/create-asset-profile.dto.ts
  8. 5
      libs/common/src/lib/dtos/create-order.dto.ts
  9. 5
      libs/common/src/lib/dtos/update-order.dto.ts
  10. 47
      libs/common/src/lib/helper.spec.ts
  11. 16
      libs/common/src/lib/helper.ts
  12. 13
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  13. 12
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  14. 14
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  15. 58
      package-lock.json
  16. 4
      package.json

4
CHANGELOG.md

@ -9,11 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the label of the cash positions in the holdings charts and table
- Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0`
- Upgraded `ai` from version `7.0.37` to `7.0.114`
- Upgraded `dotenv` from version `17.4.2` to `18.0.3`
### Fixed
- Fixed the positive performance from the all time high in the watchlist
- Fixed the portfolio calculation for holdings with historical market prices between the chart dates
## 3.77.0 - 2026-10-02

33
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -791,6 +791,16 @@ export abstract class PortfolioCalculator {
activitiesByDate[activity.date].push(activity);
}
// Carry forward the market prices of all dates, not only of the chart
// dates, as sparse market data (e.g. MANUAL) can be between two chart dates
const marketPriceDates = Object.keys(marketSymbolMap)
.filter((date) => {
return !!marketSymbolMap[date][assetProfileIdentifier];
})
.sort();
let indexOfMarketPriceDate = 0;
for (const dateString of chartDates) {
if (dateString < startDateString) {
continue;
@ -798,6 +808,18 @@ export abstract class PortfolioCalculator {
break;
}
while (
indexOfMarketPriceDate < marketPriceDates.length &&
marketPriceDates[indexOfMarketPriceDate] <= dateString
) {
lastMarketPrice =
marketSymbolMap[marketPriceDates[indexOfMarketPriceDate]][
assetProfileIdentifier
];
indexOfMarketPriceDate += 1;
}
const activitiesOfDate = activitiesByDate[dateString];
if (!lastMarketPrice && activitiesOfDate?.length > 0) {
@ -808,13 +830,8 @@ export abstract class PortfolioCalculator {
}
}
const marketPrice = marketSymbolMap[dateString]?.[assetProfileIdentifier];
const unitPrice =
marketPrice ??
lastMarketPrice ??
lastActivityUnitPrice ??
unitPriceAtEndDate;
lastMarketPrice ?? lastActivityUnitPrice ?? unitPriceAtEndDate;
if (activitiesOfDate?.length > 0) {
for (const activity of activitiesOfDate) {
@ -832,10 +849,6 @@ export abstract class PortfolioCalculator {
unitPriceFromMarketData: unitPrice
});
}
if (marketPrice) {
lastMarketPrice = marketPrice;
}
}
// Sort the activities so that the start and end placeholder activities

170
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-with-market-price-between-chart-dates.spec.ts

@ -0,0 +1,170 @@
import {
activityDummyData,
assetProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Activity } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
PortfolioSnapshotServiceMock.reset();
RedisCacheServiceMock.reset();
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
it('with MANUAL buy and market price between two chart dates', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2024-01-31').getTime());
const activities: Activity[] = [
{
...activityDummyData,
assetProfile: {
...assetProfileDummyData,
currency: 'USD',
dataSource: 'MANUAL',
name: 'Private Investment',
symbol: '6c0c5cee-0208-4975-b473-03baf2518497'
},
date: parseDate('2021-01-04'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 10,
type: 'BUY',
unitPriceInAssetProfileCurrency: 50
}
];
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const historicalDataByDate = Object.fromEntries(
portfolioSnapshot.historicalData.map((historicalDataItem) => {
return [historicalDataItem.date, historicalDataItem];
})
);
/**
* The only historical market prices are on 2023-06-14 and 2023-06-15,
* which are not chart dates (every third day from 2021-01-03)
*/
expect(historicalDataByDate['2023-06-14']).toBeUndefined();
expect(historicalDataByDate['2023-06-15']).toBeUndefined();
/**
* The chart dates before the market prices use the unit price of the
* activity: 50
*/
expect(historicalDataByDate['2023-06-13']).toMatchObject({
netPerformance: 0, // 10 * (50 - 50) = 0
totalInvestment: 500,
value: 500 // 10 * 50 = 500
});
/**
* The chart dates after the market prices carry the latest one forward
* (100), not the first one (80)
*/
expect(historicalDataByDate['2023-06-16']).toMatchObject({
netPerformance: 500, // 10 * (100 - 50) = 500
totalInvestment: 500,
value: 1000 // 10 * 100 = 1000
});
expect(historicalDataByDate['2024-01-30']).toMatchObject({
netPerformance: 500, // 10 * (100 - 50) = 500
totalInvestment: 500,
value: 1000 // 10 * 100 = 1000
});
/**
* The market price is unchanged since 2023-06-15, hence there is no
* performance today
*/
expect(portfolioSnapshot.positions[0]).toMatchObject({
netPerformancePercentageWithCurrencyEffectMap: {
'1d': new Big(0)
},
netPerformanceWithCurrencyEffectMap: {
'1d': new Big(0),
max: new Big(500) // 10 * (100 - 50) = 500
}
});
});
});
});

14
apps/api/src/app/portfolio/current-rate.service.mock.ts

@ -30,6 +30,20 @@ function mockGetValue({
return { marketPrice: 0 };
case '6c0c5cee-0208-4975-b473-03baf2518497':
// Sparse market data with the quote of today, which falls back to the
// latest market price
if (isSameDay(parseDate('2023-06-14'), date)) {
return { marketPrice: 80 };
} else if (
isSameDay(parseDate('2023-06-15'), date) ||
isSameDay(parseDate('2024-01-31'), date)
) {
return { marketPrice: 100 };
}
return { marketPrice: 0 };
case 'BALN.SW':
if (isSameDay(parseDate('2021-11-12'), date)) {
return { marketPrice: 146 };

3
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -11,6 +11,7 @@ import {
convertValuesToPercentagesOfTotal,
getAssetProfileIdentifier,
getCountryName,
getHoldingName,
isCashPosition
} from '@ghostfolio/common/helper';
import {
@ -533,7 +534,7 @@ export class GfAllocationsPageComponent implements OnInit {
value,
dataSource: position.assetProfile.dataSource,
isClickable: canOpenHoldingDetail(position),
name: position.assetProfile.name ?? ''
name: getHoldingName(position.assetProfile)
};
}
}

3
apps/client/src/app/pages/public/public-page.component.ts

@ -3,6 +3,7 @@ import {
convertValuesToPercentagesOfTotal,
getAssetProfileIdentifier,
getCountryName,
getHoldingName,
isCashPosition
} from '@ghostfolio/common/helper';
import {
@ -260,7 +261,7 @@ export class GfPublicPageComponent implements OnInit {
this.symbols[symbol] = {
symbol,
value,
name: position.assetProfile.name ?? symbol
name: getHoldingName(position.assetProfile)
};
}
}

8
libs/common/src/lib/dtos/create-asset-profile.dto.ts

@ -10,17 +10,19 @@ import {
DataSource
} from '@ghostfolio/prisma/enums';
import { Type } from 'class-transformer';
import { Transform, TransformFnParams, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUrl,
MaxLength,
ValidateNested
} from 'class-validator';
import { isString } from 'lodash-es';
import { CountryDto } from './country.dto';
import { HoldingDto } from './holding.dto';
@ -92,8 +94,12 @@ export class CreateAssetProfileDto {
@ValidateNested({ each: true })
sectors?: Prisma.InputJsonArray;
@IsNotEmpty()
@IsString()
@MaxLength(SYMBOL_MAXIMUM_LENGTH)
@Transform(({ value }: TransformFnParams) =>
isString(value) ? value.trim() : value
)
symbol: string;
@IsOptional()

5
libs/common/src/lib/dtos/create-order.dto.ts

@ -18,6 +18,7 @@ import {
IsBoolean,
IsEnum,
IsISO8601,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
@ -71,8 +72,12 @@ export class CreateOrderDto {
@Min(0)
quantity: number;
@IsNotEmpty()
@IsString()
@MaxLength(SYMBOL_MAXIMUM_LENGTH)
@Transform(({ value }: TransformFnParams) =>
isString(value) ? value.trim() : value
)
symbol: string;
@ArrayUnique()

5
libs/common/src/lib/dtos/update-order.dto.ts

@ -17,6 +17,7 @@ import {
IsArray,
IsEnum,
IsISO8601,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
@ -72,8 +73,12 @@ export class UpdateOrderDto {
@Min(0)
quantity: number;
@IsNotEmpty()
@IsString()
@MaxLength(SYMBOL_MAXIMUM_LENGTH)
@Transform(({ value }: TransformFnParams) =>
isString(value) ? value.trim() : value
)
symbol: string;
@ArrayUnique()

47
libs/common/src/lib/helper.spec.ts

@ -7,6 +7,7 @@ import {
extractNumberFromString,
getCountryCodeFromCurrency,
getEmojiFlag,
getHoldingName,
getNumberFormatGroup,
getStringOrNull,
getStringOrUndefined,
@ -143,6 +144,52 @@ describe('Helper', () => {
});
});
describe('Get holding name', () => {
it('Cash position', () => {
expect(
getHoldingName({
assetSubClass: 'CASH',
assetSubClassLabel: 'Cash',
name: 'CHF',
symbol: 'CHF'
})
).toEqual('Cash (CHF)');
});
it('Cash position without asset sub class label', () => {
expect(
getHoldingName({
assetSubClass: 'CASH',
assetSubClassLabel: undefined,
name: 'CHF',
symbol: 'CHF'
})
).toEqual('CHF');
});
it('Holding with name', () => {
expect(
getHoldingName({
assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock',
name: 'Apple Inc.',
symbol: 'AAPL'
})
).toEqual('Apple Inc.');
});
it('Holding without name', () => {
expect(
getHoldingName({
assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock',
name: undefined,
symbol: 'AAPL'
})
).toEqual('AAPL');
});
});
describe('Get number format group', () => {
let languageGetter: jest.SpyInstance<string, [], any>;

16
libs/common/src/lib/helper.ts

@ -453,6 +453,22 @@ export function getEmojiFlag(aCountryCode: string) {
});
}
export function getHoldingName({
assetSubClass,
assetSubClassLabel,
name,
symbol
}: Pick<
PortfolioPosition['assetProfile'],
'assetSubClass' | 'assetSubClassLabel' | 'name' | 'symbol'
>) {
if (isCashPosition({ assetSubClass }) && assetSubClassLabel) {
return `${assetSubClassLabel} (${symbol})`;
}
return name ?? symbol;
}
export function getLocale() {
return navigator.language ?? DEFAULT_LOCALE;
}

13
libs/ui/src/lib/holdings-table/holdings-table.component.html

@ -20,7 +20,7 @@
[countryCode]="getCountryCodeForCashPosition(element)"
[dataSource]="element.assetProfile.dataSource"
[symbol]="element.assetProfile.symbol"
[tooltip]="element.assetProfile.name"
[tooltip]="getHoldingName(element.assetProfile)"
/>
</td>
</ng-container>
@ -40,9 +40,14 @@
mat-cell
[class.w-100]="mode() === 'simple'"
>
<div class="text-truncate" [title]="element.assetProfile.name">
{{ element.assetProfile.name }}
@if (element.assetProfile.name === element.assetProfile.symbol) {
@let holdingName = getHoldingName(element.assetProfile);
<div class="text-truncate" [title]="holdingName">
{{ holdingName }}
@if (
!isCashPosition(element.assetProfile) &&
element.assetProfile.name === element.assetProfile.symbol
) {
<span>({{ element.assetProfile.assetSubClassLabel }})</span>
}
</div>

12
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -1,6 +1,7 @@
import {
canOpenHoldingDetail,
getCountryCodeFromCurrency,
getHoldingName,
getLocale,
getLowercase,
isCashPosition
@ -89,6 +90,9 @@ export class GfHoldingsTableComponent {
return columns;
});
protected readonly getHoldingName = getHoldingName;
protected readonly isCashPosition = isCashPosition;
protected readonly isLoading = computed(() => {
return !this.holdings();
});
@ -104,7 +108,13 @@ export class GfHoldingsTableComponent {
});
public constructor() {
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sortingDataAccessor = (holding, path) => {
if (path === 'assetProfile.name') {
return getHoldingName(holding.assetProfile).toLocaleLowerCase();
}
return getLowercase(holding, path) as number | string;
};
// Reactive data update
effect(() => {

14
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -3,7 +3,11 @@ import {
getIntervalFromDateRange
} from '@ghostfolio/common/calculation-helper';
import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { canOpenHoldingDetail, getLocale } from '@ghostfolio/common/helper';
import {
canOpenHoldingDetail,
getHoldingName,
getLocale
} from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
PortfolioPosition
@ -399,20 +403,18 @@ export class GfTreemapChartComponent
const raw = context.raw as GfTreemapDataPoint;
const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`;
const name = raw._data.assetProfile.name;
const name = getHoldingName(raw._data.assetProfile);
const sign =
raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : '';
const symbol = raw._data.assetProfile.symbol;
const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`;
if (raw._data.valueInBaseCurrency !== null) {
const value = raw._data.valueInBaseCurrency;
return [
`${name ?? symbol} (${allocationInPercentage})`,
`${name} (${allocationInPercentage})`,
`${value?.toLocaleString(this.locale(), {
maximumFractionDigits: 2,
minimumFractionDigits: 2
@ -429,7 +431,7 @@ export class GfTreemapChartComponent
];
} else {
return [
`${name ?? symbol} (${allocationInPercentage})`,
`${name} (${allocationInPercentage})`,
'',
$localize`Performance`,
netPerformanceInPercentageWithSign

58
package-lock.json

@ -46,13 +46,13 @@
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "3.0.0",
"@openrouter/ai-sdk-provider": "3.1.0",
"@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.7",
"@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1",
"ai": "7.0.37",
"ai": "7.0.114",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
"bootstrap": "4.6.2",
@ -189,13 +189,13 @@
"license": "MIT"
},
"node_modules/@ai-sdk/gateway": {
"version": "4.0.28",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.28.tgz",
"integrity": "sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q==",
"version": "4.0.92",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.92.tgz",
"integrity": "sha512-awX24+QedfcIDTAdwkCUNG08vPJjkQI9KZLGh4Ua+vNbgeaNFJre96OIbBefOD9XPHSVpdXjwqtWmo4Yh7oczw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "4.0.3",
"@ai-sdk/provider-utils": "5.0.12",
"@ai-sdk/provider": "4.0.18",
"@ai-sdk/provider-utils": "5.0.47",
"@vercel/oidc": "3.2.0"
},
"engines": {
@ -206,9 +206,9 @@
}
},
"node_modules/@ai-sdk/provider": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.3.tgz",
"integrity": "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==",
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.18.tgz",
"integrity": "sha512-+GZJIgz1jk86pwEbb3f1BD2bdoSKyWE4Jg4YUc7NMnMozbWemSKYZcw2F4nMaO5qwsL5A8RmioAKW81YktRpIQ==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
@ -218,15 +218,16 @@
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.12.tgz",
"integrity": "sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==",
"version": "5.0.47",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.47.tgz",
"integrity": "sha512-NZQ1hMxY6Hd4ZiinjpU5HUOsyCHiTFkHw7AiM1EVN+Cd3kNTi47flZWNvFwGqnBojntB1+2eBRTeHRuVLtPhEg==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "4.0.3",
"@ai-sdk/provider": "4.0.18",
"@standard-schema/spec": "^1.1.0",
"@workflow/serde": "4.1.0",
"eventsource-parser": "^3.0.8"
"eventsource-parser": "^3.0.8",
"undici": "^7.29.0"
},
"engines": {
"node": ">=22"
@ -235,6 +236,15 @@
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/provider-utils/node_modules/undici": {
"version": "7.30.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
"integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@ -11951,9 +11961,9 @@
}
},
"node_modules/@openrouter/ai-sdk-provider": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-3.0.0.tgz",
"integrity": "sha512-m9XTSWoODH2RM5OsZpaGiN7QRR8cdP5paBWq699Tu3JVmGPBKT8xF8XwV0ZBVVsjikD/JgWfak4VSsTR4wAVbg==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-3.1.0.tgz",
"integrity": "sha512-BQy1TA9fKrh47s9ivsO6FY5QimN7GUF0i8Qon3pkxQclEgShso0dClpcWn8OIyPLEZZPFaFKgI8TW4S/O0p0Dw==",
"license": "Apache-2.0",
"engines": {
"node": ">=22"
@ -16840,14 +16850,14 @@
}
},
"node_modules/ai": {
"version": "7.0.37",
"resolved": "https://registry.npmjs.org/ai/-/ai-7.0.37.tgz",
"integrity": "sha512-stF+SEQJgKY3Qfe3FwNzqUrehHviOp2l7LemoI8YMOa0Zk5PxKsRNgUk3cHXz0RAue9RRyCAis2fz6LSCP6EKw==",
"version": "7.0.114",
"resolved": "https://registry.npmjs.org/ai/-/ai-7.0.114.tgz",
"integrity": "sha512-jRMOw7tPMWVYomoHWY5xHBXPQDyKTY1Pn3BX0Rh39UOP6y9FYq6xCIB6khgIC/b9CRg4+IxadwIbie+Eu7yU3g==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/gateway": "4.0.28",
"@ai-sdk/provider": "4.0.3",
"@ai-sdk/provider-utils": "5.0.12"
"@ai-sdk/gateway": "4.0.92",
"@ai-sdk/provider": "4.0.18",
"@ai-sdk/provider-utils": "5.0.47"
},
"engines": {
"node": ">=22"

4
package.json

@ -90,13 +90,13 @@
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "3.0.0",
"@openrouter/ai-sdk-provider": "3.1.0",
"@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.7",
"@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1",
"ai": "7.0.37",
"ai": "7.0.114",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
"bootstrap": "4.6.2",

Loading…
Cancel
Save