Browse Source

Refactoring

pull/6960/head
Thomas Kaul 3 months ago
parent
commit
3ce1a08dfd
  1. 59
      apps/api/src/services/fetch/fetch.service.ts
  2. 16
      apps/api/src/services/fetch/interfaces/web-fetch-domain.interface.ts
  3. 16
      apps/api/src/services/fetch/interfaces/web-fetch-route.interface.ts
  4. 2
      libs/common/src/lib/config.ts

59
apps/api/src/services/fetch/fetch.service.ts

@ -3,7 +3,7 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { import {
PROPERTY_API_KEY_OPENROUTER, PROPERTY_API_KEY_OPENROUTER,
PROPERTY_OPENROUTER_MODEL, PROPERTY_OPENROUTER_MODEL,
PROPERTY_WEB_FETCH_DOMAINS PROPERTY_WEB_FETCH_ROUTES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
@ -11,23 +11,21 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { generateText, jsonSchema, tool } from 'ai'; import { generateText, jsonSchema, tool } from 'ai';
import ms from 'ms'; import ms from 'ms';
import { import { WebFetchRoute } from './interfaces/web-fetch-route.interface';
HttpRequestMethod,
WebFetchDomain
} from './interfaces/web-fetch-domain.interface';
@Injectable() @Injectable()
export class FetchService implements OnModuleInit { export class FetchService implements OnModuleInit {
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token']; private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds');
private webFetchDomains: WebFetchDomain[] = []; private webFetchRoutes: WebFetchRoute[] = [];
public constructor(private readonly propertyService: PropertyService) {} public constructor(private readonly propertyService: PropertyService) {}
public async onModuleInit() { public async onModuleInit() {
this.webFetchDomains = this.webFetchRoutes =
(await this.propertyService.getByKey<WebFetchDomain[]>( (await this.propertyService.getByKey<WebFetchRoute[]>(
PROPERTY_WEB_FETCH_DOMAINS PROPERTY_WEB_FETCH_ROUTES
)) ?? []; )) ?? [];
} }
@ -39,27 +37,27 @@ export class FetchService implements OnModuleInit {
init?.method ?? init?.method ??
(input instanceof Request ? input.method : undefined) ?? (input instanceof Request ? input.method : undefined) ??
'GET' 'GET'
).toUpperCase() as HttpRequestMethod; ).toUpperCase();
const url = input instanceof Request ? input.url : input.toString(); const url = input instanceof Request ? input.url : input.toString();
const urlRedacted = this.redactUrl(url); const urlRedacted = this.redactUrl(url);
const matchedWebFetchDomain = this.getMatchingWebFetchDomain({
method,
url
});
Logger.debug(`${method} ${urlRedacted}`, 'FetchService'); Logger.debug(`${method} ${urlRedacted}`, 'FetchService');
if (matchedWebFetchDomain) { if (method === 'GET') {
const matchedWebFetchRoute = this.getMatchingWebFetchRoute(url);
if (matchedWebFetchRoute) {
const response = await this.fetchViaWebFetchTool( const response = await this.fetchViaWebFetchTool(
url, url,
matchedWebFetchDomain matchedWebFetchRoute
); );
if (response) { if (response) {
return response; return response;
} }
} }
}
try { try {
return await globalThis.fetch(input, init); return await globalThis.fetch(input, init);
@ -82,7 +80,7 @@ export class FetchService implements OnModuleInit {
private async fetchViaWebFetchTool( private async fetchViaWebFetchTool(
url: string, url: string,
webFetchDomain: WebFetchDomain webFetchRoute: WebFetchRoute
): Promise<Response | undefined> { ): Promise<Response | undefined> {
const [openRouterApiKey, openRouterModel] = await Promise.all([ const [openRouterApiKey, openRouterModel] = await Promise.all([
this.propertyService.getByKey<string>(PROPERTY_API_KEY_OPENROUTER), this.propertyService.getByKey<string>(PROPERTY_API_KEY_OPENROUTER),
@ -97,14 +95,18 @@ export class FetchService implements OnModuleInit {
const openRouterService = createOpenRouter({ apiKey: openRouterApiKey }); const openRouterService = createOpenRouter({ apiKey: openRouterApiKey });
const { sources, text } = await generateText({ const { sources, text } = await generateText({
abortSignal: AbortSignal.timeout(ms('30 seconds')),
model: openRouterService.chat(openRouterModel), model: openRouterService.chat(openRouterModel),
prompt: [ prompt: [
'You have access to a web_fetch tool. You MUST call it to retrieve the URL below, do not answer from prior knowledge.', 'You have access to a web_fetch tool. You MUST call it to retrieve the URL below, do not answer from prior knowledge.',
'Return the fetched response body exactly as received: raw body only, no commentary, no Markdown, and no code fences.', 'Return the fetched response body exactly as received: raw body only, no commentary, no Markdown, and no code fences.',
`URL: ${url}` `URL: ${url}`
].join('\n'), ].join('\n'),
timeout: FetchService.WEB_FETCH_TIMEOUT,
tools: { tools: {
// Provider-defined tool: lets OpenRouter perform the actual web
// request server-side via its `web_fetch` engine. `id` and `args`
// are the OpenRouter-specific identifiers; the input schema is left
// open as the arguments are supplied by the model.
web_fetch: tool({ web_fetch: tool({
args: { engine: 'openrouter' }, args: { engine: 'openrouter' },
id: 'openrouter.web_fetch', id: 'openrouter.web_fetch',
@ -135,7 +137,7 @@ export class FetchService implements OnModuleInit {
continue; continue;
} }
if (webFetchDomain.responseContentType.includes('application/json')) { if (webFetchRoute.responseContentType?.includes('application/json')) {
try { try {
JSON.parse(body); JSON.parse(body);
} catch { } catch {
@ -149,7 +151,9 @@ export class FetchService implements OnModuleInit {
); );
return new Response(body, { return new Response(body, {
headers: { 'content-type': webFetchDomain.responseContentType } headers: webFetchRoute.responseContentType
? { 'content-type': webFetchRoute.responseContentType }
: undefined
}); });
} }
@ -166,21 +170,12 @@ export class FetchService implements OnModuleInit {
} }
} }
private getMatchingWebFetchDomain({ private getMatchingWebFetchRoute(url: string) {
method,
url
}: {
method: HttpRequestMethod;
url: string;
}) {
try { try {
const { hostname } = new URL(url); const { hostname } = new URL(url);
return this.webFetchDomains.find(({ domain, methods }) => { return this.webFetchRoutes.find(({ domain }) => {
const matchesDomain = return hostname === domain || hostname.endsWith(`.${domain}`);
hostname === domain || hostname.endsWith(`.${domain}`);
return matchesDomain && methods.includes(method);
}); });
} catch { } catch {
return undefined; return undefined;

16
apps/api/src/services/fetch/interfaces/web-fetch-domain.interface.ts

@ -1,16 +0,0 @@
export type HttpRequestMethod =
| 'CONNECT'
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT'
| 'TRACE';
export interface WebFetchDomain {
domain: string;
methods: HttpRequestMethod[];
responseContentType: string;
}

16
apps/api/src/services/fetch/interfaces/web-fetch-route.interface.ts

@ -0,0 +1,16 @@
/**
* Routes outgoing GET requests for a given domain through the OpenRouter
* `web_fetch` tool instead of a direct network request.
*
* Configured via the `WEB_FETCH_ROUTES` property as a JSON array, e.g.
*
* [
* { "domain": "example.com", "responseContentType": "application/json" }
* ]
*
* Matches the domain itself and its subdomains (e.g. `api.example.com`).
*/
export interface WebFetchRoute {
domain: string;
responseContentType?: string;
}

2
libs/common/src/lib/config.ts

@ -256,7 +256,7 @@ export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS';
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';
export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE'; export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE';
export const PROPERTY_UPTIME = 'UPTIME'; export const PROPERTY_UPTIME = 'UPTIME';
export const PROPERTY_WEB_FETCH_DOMAINS = 'WEB_FETCH_DOMAINS'; export const PROPERTY_WEB_FETCH_ROUTES = 'WEB_FETCH_ROUTES';
export const QUEUE_JOB_STATUS_LIST = [ export const QUEUE_JOB_STATUS_LIST = [
'active', 'active',

Loading…
Cancel
Save