Browse Source

Task/upgrade ai to version 6.0.174 (#6828)

* Upgrade ai

* @openrouter/ai-sdk-provider to version 2.9.0
* ai to version 6.0.174

* Add AI service health check and improve layout

* Update changelog
pull/6787/head^2
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
4da5c3cac9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 1
      apps/api/src/app/endpoints/ai/ai.module.ts
  3. 13
      apps/api/src/app/endpoints/ai/ai.service.ts
  4. 31
      apps/api/src/app/health/health.controller.ts
  5. 2
      apps/api/src/app/health/health.module.ts
  6. 65
      apps/client/src/app/pages/api/api-page.component.ts
  7. 178
      apps/client/src/app/pages/api/api-page.html
  8. 5
      apps/client/src/app/pages/api/interfaces/interfaces.ts
  9. 2
      libs/common/src/lib/interfaces/index.ts
  10. 3
      libs/common/src/lib/interfaces/responses/ai-service-health-response.interface.ts
  11. 202
      package-lock.json
  12. 4
      package.json

2
CHANGELOG.md

@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Migrated various components from `NgClass` to class bindings
- Refreshed the cryptocurrencies list
- Upgraded `@ionic/angular` from version `8.8.1` to `8.8.5`
- Upgraded `@openrouter/ai-sdk-provider` from version `0.7.2` to `2.9.0`
- Upgraded `ai` from version `4.3.16` to `6.0.174`
- Upgraded `bull-board` from version `6.20.3` to `7.0.0`
- Upgraded `countries-and-timezones` from version `3.8.0` to `3.9.0`
- Upgraded `fuse.js` from version `7.1.0` to `7.3.0`

1
apps/api/src/app/endpoints/ai/ai.module.ts

@ -28,6 +28,7 @@ import { AiService } from './ai.service';
@Module({
controllers: [AiController],
exports: [AiService],
imports: [
ActivitiesModule,
ApiModule,

13
apps/api/src/app/endpoints/ai/ai.service.ts

@ -1,4 +1,5 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
PROPERTY_API_KEY_OPENROUTER,
@ -36,11 +37,18 @@ export class AiService {
];
public constructor(
private readonly configurationService: ConfigurationService,
private readonly portfolioService: PortfolioService,
private readonly propertyService: PropertyService
) {}
public async generateText({ prompt }: { prompt: string }) {
public async generateText({
prompt,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: {
prompt: string;
requestTimeout?: number;
}) {
const openRouterApiKey = await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_OPENROUTER
);
@ -55,7 +63,8 @@ export class AiService {
return generateText({
prompt,
model: openRouterService.chat(openRouterModel)
model: openRouterService.chat(openRouterModel),
timeout: requestTimeout
});
}

31
apps/api/src/app/health/health.controller.ts

@ -1,5 +1,7 @@
import { AiService } from '@ghostfolio/api/app/endpoints/ai/ai.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import {
AiServiceHealthResponse,
DataEnhancerHealthResponse,
DataProviderHealthResponse
} from '@ghostfolio/common/interfaces';
@ -9,6 +11,7 @@ import {
Get,
HttpException,
HttpStatus,
Logger,
Param,
Res,
UseInterceptors
@ -21,7 +24,10 @@ import { HealthService } from './health.service';
@Controller('health')
export class HealthController {
public constructor(private readonly healthService: HealthService) {}
public constructor(
private readonly aiService: AiService,
private readonly healthService: HealthService
) {}
@Get()
public async getHealth(@Res() response: Response) {
@ -40,6 +46,29 @@ export class HealthController {
}
}
@Get('ai')
public async getHealthOfAiService(
@Res() response: Response
): Promise<Response<AiServiceHealthResponse>> {
try {
const { text } = await this.aiService.generateText({
prompt: `Reply with the word "OK" and nothing else.`
});
if (text === 'OK') {
return response
.status(HttpStatus.OK)
.json({ status: getReasonPhrase(StatusCodes.OK) });
}
} catch (error) {
Logger.error(error, 'HealthController');
}
return response
.status(HttpStatus.SERVICE_UNAVAILABLE)
.json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) });
}
@Get('data-enhancer/:name')
public async getHealthOfDataEnhancer(
@Param('name') name: string,

2
apps/api/src/app/health/health.module.ts

@ -1,3 +1,4 @@
import { AiModule } from '@ghostfolio/api/app/endpoints/ai/ai.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
@ -12,6 +13,7 @@ import { HealthService } from './health.service';
@Module({
controllers: [HealthController],
imports: [
AiModule,
DataEnhancerModule,
DataProviderModule,
PropertyModule,

65
apps/client/src/app/pages/api/api-page.component.ts

@ -4,6 +4,7 @@ import {
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AiServiceHealthResponse,
DataProviderGhostfolioAssetProfileResponse,
DataProviderGhostfolioStatusResponse,
DividendsResponse,
@ -13,27 +14,42 @@ import {
} from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import {
HttpClient,
HttpErrorResponse,
HttpHeaders,
HttpParams
} from '@angular/common/http';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatCardModule } from '@angular/material/card';
import { format, startOfYear } from 'date-fns';
import { map, Observable } from 'rxjs';
import { isObject } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { catchError, map, Observable, of, OperatorFunction } from 'rxjs';
import { FetchFailure, FetchResult } from './interfaces/interfaces';
@Component({
host: { class: 'page' },
imports: [CommonModule],
imports: [CommonModule, MatCardModule, NgxSkeletonLoaderModule],
selector: 'gf-api-page',
styleUrls: ['./api-page.scss'],
templateUrl: './api-page.html'
})
export class GfApiPageComponent implements OnInit {
public assetProfile$: Observable<DataProviderGhostfolioAssetProfileResponse>;
public dividends$: Observable<DividendsResponse['dividends']>;
public historicalData$: Observable<HistoricalResponse['historicalData']>;
public isinLookupItems$: Observable<LookupResponse['items']>;
public lookupItems$: Observable<LookupResponse['items']>;
public quotes$: Observable<QuotesResponse['quotes']>;
public status$: Observable<DataProviderGhostfolioStatusResponse>;
public aiServiceHealth$: Observable<FetchResult<AiServiceHealthResponse>>;
public assetProfile$: Observable<
FetchResult<DataProviderGhostfolioAssetProfileResponse>
>;
public dividends$: Observable<FetchResult<DividendsResponse['dividends']>>;
public historicalData$: Observable<
FetchResult<HistoricalResponse['historicalData']>
>;
public isinLookupItems$: Observable<FetchResult<LookupResponse['items']>>;
public lookupItems$: Observable<FetchResult<LookupResponse['items']>>;
public quotes$: Observable<FetchResult<QuotesResponse['quotes']>>;
public status$: Observable<FetchResult<DataProviderGhostfolioStatusResponse>>;
private apiKey: string;
@ -45,6 +61,7 @@ export class GfApiPageComponent implements OnInit {
public ngOnInit() {
this.apiKey = prompt($localize`Please enter your Ghostfolio API key:`);
this.aiServiceHealth$ = this.fetchAiServiceHealth();
this.assetProfile$ = this.fetchAssetProfile({ symbol: 'AAPL' });
this.dividends$ = this.fetchDividends({ symbol: 'KO' });
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' });
@ -54,13 +71,33 @@ export class GfApiPageComponent implements OnInit {
this.status$ = this.fetchStatus();
}
public isFetchFailure(value: unknown): value is FetchFailure {
return isObject(value) && value !== null && 'fetchError' in value;
}
private catchFetchFailure<T>(): OperatorFunction<T, T | FetchFailure> {
return catchError(({ error }: HttpErrorResponse) => {
const body = error as { message?: string; status?: string };
const status = body?.status ?? 'Error';
const fetchError = body?.message ? `${status}: ${body.message}` : status;
return of<FetchFailure>({ fetchError });
}) as OperatorFunction<T, T | FetchFailure>;
}
private fetchAiServiceHealth() {
return this.http
.get<AiServiceHealthResponse>('/api/v1/health/ai')
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private fetchAssetProfile({ symbol }: { symbol: string }) {
return this.http
.get<DataProviderGhostfolioAssetProfileResponse>(
`/api/v1/data-providers/ghostfolio/asset-profile/${symbol}`,
{ headers: this.getHeaders() }
)
.pipe(takeUntilDestroyed(this.destroyRef));
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private fetchDividends({ symbol }: { symbol: string }) {
@ -80,6 +117,7 @@ export class GfApiPageComponent implements OnInit {
map(({ dividends }) => {
return dividends;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -101,6 +139,7 @@ export class GfApiPageComponent implements OnInit {
map(({ historicalData }) => {
return historicalData;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -127,6 +166,7 @@ export class GfApiPageComponent implements OnInit {
map(({ items }) => {
return items;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -143,6 +183,7 @@ export class GfApiPageComponent implements OnInit {
map(({ quotes }) => {
return quotes;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -153,7 +194,7 @@ export class GfApiPageComponent implements OnInit {
'/api/v2/data-providers/ghostfolio/status',
{ headers: this.getHeaders() }
)
.pipe(takeUntilDestroyed(this.destroyRef));
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private getHeaders() {

178
apps/client/src/app/pages/api/api-page.html

@ -1,38 +1,113 @@
<div class="container">
<div class="mb-3">
<h2 class="text-center">Status</h2>
<div>{{ status$ | async | json }}</div>
</div>
<div class="mb-3">
<h2 class="text-center">Asset Profile</h2>
<div>{{ assetProfile$ | async | json }}</div>
</div>
<div>
<h2 class="text-center">Lookup</h2>
@if (lookupItems$) {
<div class="mb-5 row">
<div class="col">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>AI Service Health</mat-card-title>
</mat-card-header>
<mat-card-content>
@let aiServiceHealth = aiServiceHealth$ | async;
@if (isFetchFailure(aiServiceHealth)) {
🔴 {{ aiServiceHealth.fetchError }}
} @else if (aiServiceHealth) {
🟢 {{ aiServiceHealth.status }}
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Status</mat-card-title>
</mat-card-header>
<mat-card-content>
@let status = status$ | async;
@if (isFetchFailure(status)) {
🔴 {{ status.fetchError }}
} @else if (status) {
<pre><code>{{ status | json }}</code></pre>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Asset Profile</mat-card-title>
</mat-card-header>
<mat-card-content>
@let assetProfile = assetProfile$ | async;
@if (isFetchFailure(assetProfile)) {
🔴 {{ assetProfile.fetchError }}
} @else if (assetProfile) {
<pre><code>{{ assetProfile | json }}</code></pre>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Lookup</mat-card-title>
</mat-card-header>
<mat-card-content>
@let symbols = lookupItems$ | async;
@if (isFetchFailure(symbols)) {
🔴 {{ symbols.fetchError }}
} @else if (symbols) {
<ul>
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</div>
<div>
<h2 class="text-center">Lookup (ISIN)</h2>
@if (isinLookupItems$) {
@let symbols = isinLookupItems$ | async;
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Lookup (ISIN)</mat-card-title>
</mat-card-header>
<mat-card-content>
@let isinSymbols = isinLookupItems$ | async;
@if (isFetchFailure(isinSymbols)) {
🔴 {{ isinSymbols.fetchError }}
} @else if (isinSymbols) {
<ul>
@for (item of symbols; track item.symbol) {
@for (item of isinSymbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</div>
<div>
<h2 class="text-center">Quotes</h2>
@if (quotes$) {
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Quotes</mat-card-title>
</mat-card-header>
<mat-card-content>
@let quotes = quotes$ | async;
@if (isFetchFailure(quotes)) {
🔴 {{ quotes.fetchError }}
} @else if (quotes) {
<ul>
@for (quote of quotes | keyvalue; track quote) {
<li>
@ -41,37 +116,58 @@
</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</div>
<div>
<h2 class="text-center">Historical</h2>
@if (historicalData$) {
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Historical</mat-card-title>
</mat-card-header>
<mat-card-content>
@let historicalData = historicalData$ | async;
@if (isFetchFailure(historicalData)) {
🔴 {{ historicalData.fetchError }}
} @else if (historicalData) {
<ul>
@for (
historicalDataItem of historicalData | keyvalue;
track historicalDataItem
) {
<li>
{{ historicalDataItem.key }}:
{{ historicalDataItem.value.marketPrice }}
</li>
@for (item of historicalData | keyvalue; track item) {
<li>{{ item.key }}: {{ item.value.marketPrice }}</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</div>
<div>
<h2 class="text-center">Dividends</h2>
@if (dividends$) {
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Dividends</mat-card-title>
</mat-card-header>
<mat-card-content>
@let dividends = dividends$ | async;
@if (isFetchFailure(dividends)) {
🔴 {{ dividends.fetchError }}
} @else if (dividends) {
<ul>
@for (dividend of dividends | keyvalue; track dividend) {
<li>
{{ dividend.key }}:
{{ dividend.value.marketPrice }}
</li>
<li>{{ dividend.key }}: {{ dividend.value.marketPrice }}</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
</div>
</div>
</div>

5
apps/client/src/app/pages/api/interfaces/interfaces.ts

@ -0,0 +1,5 @@
export interface FetchFailure {
fetchError: string;
}
export type FetchResult<T> = T | FetchFailure;

2
libs/common/src/lib/interfaces/index.ts

@ -44,6 +44,7 @@ import type { ActivityResponse } from './responses/activity-response.interface';
import type { AdminUserResponse } from './responses/admin-user-response.interface';
import type { AdminUsersResponse } from './responses/admin-users-response.interface';
import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { AiServiceHealthResponse } from './responses/ai-service-health-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { AssetResponse } from './responses/asset-response.interface';
import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface';
@ -117,6 +118,7 @@ export {
AdminUserResponse,
AdminUsersResponse,
AiPromptResponse,
AiServiceHealthResponse,
ApiKeyResponse,
AssertionCredentialJSON,
AssetClassSelectorOption,

3
libs/common/src/lib/interfaces/responses/ai-service-health-response.interface.ts

@ -0,0 +1,3 @@
export interface AiServiceHealthResponse {
status: string;
}

202
package-lock.json

@ -40,12 +40,12 @@
"@nestjs/platform-express": "11.1.19",
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@openrouter/ai-sdk-provider": "0.7.2",
"@openrouter/ai-sdk-provider": "2.9.0",
"@prisma/adapter-pg": "7.7.0",
"@prisma/client": "7.7.0",
"@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2",
"ai": "4.3.16",
"ai": "6.0.174",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
"bootstrap": "4.6.2",
@ -175,74 +175,50 @@
"dev": true,
"license": "MIT"
},
"node_modules/@ai-sdk/provider": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz",
"integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz",
"integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==",
"node_modules/@ai-sdk/gateway": {
"version": "3.0.109",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.109.tgz",
"integrity": "sha512-r6dOqThjODp1vOhGRJg2OCmyB/ZOQtGx1esZ2SDvwDX5XoX8dBqYaYjLg8MPXTzMGJSgOkJyCxWgUcZtAl16pw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"nanoid": "^3.3.8",
"secure-json-parse": "^2.7.0"
"@ai-sdk/provider": "3.0.10",
"@ai-sdk/provider-utils": "4.0.26",
"@vercel/oidc": "3.2.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.23.8"
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/react": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz",
"integrity": "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==",
"node_modules/@ai-sdk/provider": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz",
"integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider-utils": "2.2.8",
"@ai-sdk/ui-utils": "1.2.11",
"swr": "^2.2.5",
"throttleit": "2.1.0"
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@ai-sdk/ui-utils": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz",
"integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==",
"node_modules/@ai-sdk/provider-utils": {
"version": "4.0.26",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.26.tgz",
"integrity": "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"@ai-sdk/provider-utils": "2.2.8",
"zod-to-json-schema": "^3.24.1"
"@ai-sdk/provider": "3.0.10",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.8"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.23.8"
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@algolia/abtesting": {
@ -10810,20 +10786,16 @@
}
},
"node_modules/@openrouter/ai-sdk-provider": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-0.7.2.tgz",
"integrity": "sha512-Fry2mV7uGGJRmP9JntTZRc8ElESIk7AJNTacLbF6Syoeb5k8d7HPGkcK9rTXDlqBb8HgU1hOKtz23HojesTmnw==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.9.0.tgz",
"integrity": "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"@ai-sdk/provider-utils": "2.2.8"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"ai": "^4.3.16",
"zod": "^3.25.34"
"ai": "^6.0.0",
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/@opentelemetry/api": {
@ -14455,12 +14427,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@ -15986,6 +15952,15 @@
"d3-transition": "^3.0.1"
}
},
"node_modules/@vercel/oidc": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz",
"integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==",
"license": "Apache-2.0",
"engines": {
"node": ">= 20"
}
},
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz",
@ -16440,29 +16415,21 @@
}
},
"node_modules/ai": {
"version": "4.3.16",
"resolved": "https://registry.npmjs.org/ai/-/ai-4.3.16.tgz",
"integrity": "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==",
"version": "6.0.174",
"resolved": "https://registry.npmjs.org/ai/-/ai-6.0.174.tgz",
"integrity": "sha512-bTrfLUWHWtkjzWyCY4bmyuk4Qvmj4S4NSNsXyNSVVqkmftQNtxRj7dzUoMeQDBBwlJO6fC7m2Q/lNOPqQQfAGA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"@ai-sdk/provider-utils": "2.2.8",
"@ai-sdk/react": "1.2.12",
"@ai-sdk/ui-utils": "1.2.11",
"@opentelemetry/api": "1.9.0",
"jsondiffpatch": "0.6.0"
"@ai-sdk/gateway": "3.0.109",
"@ai-sdk/provider": "3.0.10",
"@ai-sdk/provider-utils": "4.0.26",
"@opentelemetry/api": "1.9.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/ajv": {
@ -20844,7 +20811,9 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@ -20922,12 +20891,6 @@
"node": ">=0.3.1"
}
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
"license": "Apache-2.0"
},
"node_modules/dns-packet": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@ -22137,10 +22100,9 @@
}
},
"node_modules/eventsource-parser": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"dev": true,
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
"integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@ -28868,35 +28830,6 @@
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"license": "MIT"
},
"node_modules/jsondiffpatch": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
"integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
"license": "MIT",
"dependencies": {
"@types/diff-match-patch": "^1.0.36",
"chalk": "^5.3.0",
"diff-match-patch": "^1.0.5"
},
"bin": {
"jsondiffpatch": "bin/jsondiffpatch.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/jsondiffpatch/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
@ -29727,6 +29660,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@ -33634,6 +33568,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@ -35337,12 +35272,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -36838,19 +36767,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/swr": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
"integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -37254,18 +37170,6 @@
"tslib": "^2"
}
},
"node_modules/throttleit": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
"integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
@ -38654,6 +38558,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@ -40490,6 +40395,7 @@
"version": "3.25.2",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
"dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.25.28 || ^4"

4
package.json

@ -85,12 +85,12 @@
"@nestjs/platform-express": "11.1.19",
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@openrouter/ai-sdk-provider": "0.7.2",
"@openrouter/ai-sdk-provider": "2.9.0",
"@prisma/adapter-pg": "7.7.0",
"@prisma/client": "7.7.0",
"@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2",
"ai": "4.3.16",
"ai": "6.0.174",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
"bootstrap": "4.6.2",

Loading…
Cancel
Save