|
|
|
@ -1,7 +1,9 @@ |
|
|
|
import { redactPaths } from '@ghostfolio/api/helper/object.helper'; |
|
|
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; |
|
|
|
import { |
|
|
|
DEFAULT_OPENROUTER_ENGINE_WEB_FETCH, |
|
|
|
PROPERTY_API_KEY_OPENROUTER, |
|
|
|
PROPERTY_OPENROUTER_ENGINE_WEB_FETCH, |
|
|
|
PROPERTY_OPENROUTER_MODEL, |
|
|
|
PROPERTY_OPENROUTER_MODEL_WEB_FETCH, |
|
|
|
PROPERTY_PROXY_ROUTES, |
|
|
|
@ -20,6 +22,7 @@ import { WebFetchRoute } from './interfaces/web-fetch-route.interface'; |
|
|
|
export class FetchService implements OnModuleInit { |
|
|
|
private readonly logger = new Logger(FetchService.name); |
|
|
|
|
|
|
|
private static readonly BODY_PREVIEW_LENGTH = 500; |
|
|
|
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token']; |
|
|
|
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds'); |
|
|
|
|
|
|
|
@ -91,15 +94,22 @@ export class FetchService implements OnModuleInit { |
|
|
|
url: string; |
|
|
|
webFetchRoute: WebFetchRoute; |
|
|
|
}) { |
|
|
|
const [openRouterApiKey, openRouterModel, openRouterModelWebFetch] = |
|
|
|
await Promise.all([ |
|
|
|
const [ |
|
|
|
openRouterApiKey, |
|
|
|
openRouterEngineWebFetch, |
|
|
|
openRouterModel, |
|
|
|
openRouterModelWebFetch |
|
|
|
] = await Promise.all([ |
|
|
|
this.propertyService.getByKey<string>(PROPERTY_API_KEY_OPENROUTER), |
|
|
|
this.propertyService.getByKey<string>(PROPERTY_OPENROUTER_MODEL), |
|
|
|
this.propertyService.getByKey<string>( |
|
|
|
PROPERTY_OPENROUTER_MODEL_WEB_FETCH |
|
|
|
) |
|
|
|
PROPERTY_OPENROUTER_ENGINE_WEB_FETCH |
|
|
|
), |
|
|
|
this.propertyService.getByKey<string>(PROPERTY_OPENROUTER_MODEL), |
|
|
|
this.propertyService.getByKey<string>(PROPERTY_OPENROUTER_MODEL_WEB_FETCH) |
|
|
|
]); |
|
|
|
|
|
|
|
const engine = |
|
|
|
openRouterEngineWebFetch || DEFAULT_OPENROUTER_ENGINE_WEB_FETCH; |
|
|
|
const model = openRouterModelWebFetch || openRouterModel; |
|
|
|
|
|
|
|
if (!model || !openRouterApiKey) { |
|
|
|
@ -119,11 +129,15 @@ export class FetchService implements OnModuleInit { |
|
|
|
timeout: FetchService.WEB_FETCH_TIMEOUT, |
|
|
|
tools: { |
|
|
|
// Provider-executed 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.
|
|
|
|
// request server-side via its `web_fetch` engine. `args` and `id`
|
|
|
|
// are the OpenRouter-specific identifiers. The engine must stay
|
|
|
|
// nested in `parameters`, because the provider spreads `args` on the
|
|
|
|
// top level of the tool object, where OpenRouter ignores it. Nested
|
|
|
|
// keys need snake case, as only the top level gets converted. The
|
|
|
|
// input schema is left open as the arguments are supplied by the
|
|
|
|
// model.
|
|
|
|
web_fetch: tool({ |
|
|
|
args: { engine: 'openrouter' }, |
|
|
|
args: { parameters: { engine } }, |
|
|
|
id: 'openrouter.web_fetch', |
|
|
|
inputSchema: jsonSchema({ |
|
|
|
additionalProperties: true, |
|
|
|
@ -142,6 +156,8 @@ export class FetchService implements OnModuleInit { |
|
|
|
text |
|
|
|
]; |
|
|
|
|
|
|
|
let rejectedBody: string; |
|
|
|
|
|
|
|
for (const candidate of candidates) { |
|
|
|
if (typeof candidate !== 'string') { |
|
|
|
continue; |
|
|
|
@ -157,6 +173,8 @@ export class FetchService implements OnModuleInit { |
|
|
|
try { |
|
|
|
JSON.parse(body); |
|
|
|
} catch { |
|
|
|
rejectedBody = body; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -170,6 +188,16 @@ export class FetchService implements OnModuleInit { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
this.logger.warn( |
|
|
|
`Web fetch tool returned no usable body for ${this.redactUrl(url)}` |
|
|
|
); |
|
|
|
|
|
|
|
this.logger.debug( |
|
|
|
`Web fetch tool response for ${this.redactUrl(url)} (${ |
|
|
|
sources?.length ?? 0 |
|
|
|
} sources): ${this.getBodyPreview(rejectedBody || text)}` |
|
|
|
); |
|
|
|
|
|
|
|
return undefined; |
|
|
|
} catch (error) { |
|
|
|
this.logger.error( |
|
|
|
@ -227,6 +255,22 @@ export class FetchService implements OnModuleInit { |
|
|
|
: requestUrl.toString(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Makes a short single-line preview of a response body for the log. Gives |
|
|
|
* back a placeholder if the body is empty. |
|
|
|
*/ |
|
|
|
private getBodyPreview(body: string): string { |
|
|
|
if (!body) { |
|
|
|
return '(empty)'; |
|
|
|
} |
|
|
|
|
|
|
|
const singleLineBody = body.replace(/\s+/g, ' ').trim(); |
|
|
|
|
|
|
|
return singleLineBody.length > FetchService.BODY_PREVIEW_LENGTH |
|
|
|
? `${singleLineBody.slice(0, FetchService.BODY_PREVIEW_LENGTH)}…` |
|
|
|
: singleLineBody; |
|
|
|
} |
|
|
|
|
|
|
|
private getMatchingWebFetchRoute(url: string) { |
|
|
|
try { |
|
|
|
const { hostname } = new URL(url); |
|
|
|
|