@outputai/core 0.15.1-next.59eb401.0 → 0.15.1-next.8ef04e2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +58 -6
- package/src/helpers/fetch.js +21 -9
- package/src/helpers/path.js +36 -0
- package/src/helpers/redact.js +2 -1
- package/src/http/config.js +10 -0
- package/src/http/cost.d.ts +20 -0
- package/src/http/cost.js +41 -0
- package/src/http/fetch/events.js +10 -0
- package/src/http/fetch/index.d.ts +26 -0
- package/src/http/fetch/index.js +78 -0
- package/src/http/fetch/logger.d.ts +10 -0
- package/src/http/fetch/logger.js +56 -0
- package/src/http/index.d.ts +27 -0
- package/src/http/index.js +9 -0
- package/src/http/ky/index.d.ts +25 -0
- package/src/http/ky/index.js +10 -0
- package/src/http/request_tag.js +30 -0
- package/src/llm/agent.js +141 -0
- package/src/llm/ai_provider.js +131 -0
- package/src/llm/ai_sdk_options.js +126 -0
- package/src/llm/consts.js +6 -0
- package/src/llm/deprecated_provider_aliases.js +8 -0
- package/src/llm/generate.js +88 -0
- package/src/llm/index.d.ts +631 -0
- package/src/llm/index.js +5 -0
- package/src/llm/prompt/content.js +70 -0
- package/src/llm/prompt/interpolations.js +58 -0
- package/src/llm/prompt/loader.js +67 -0
- package/src/llm/prompt/markup/attributes.js +52 -0
- package/src/llm/prompt/markup/nodes.js +71 -0
- package/src/llm/prompt/markup/tokenizer.js +3 -0
- package/src/llm/prompt/markup/tokens.js +26 -0
- package/src/llm/prompt/validations.js +129 -0
- package/src/llm/utils/cost.js +150 -0
- package/src/llm/utils/error_handler.js +53 -0
- package/src/llm/utils/file.js +42 -0
- package/src/llm/utils/google_vertex_auth.js +31 -0
- package/src/llm/utils/grounding.js +49 -0
- package/src/llm/utils/image.js +37 -0
- package/src/llm/utils/legacy_cost_attribute.js +75 -0
- package/src/llm/utils/metering.js +126 -0
- package/src/llm/utils/models.js +15 -0
- package/src/llm/utils/models_pricing.js +93 -0
- package/src/llm/utils/models_pricing_snapshot.json +4166 -0
- package/src/llm/utils/skills.js +74 -0
- package/src/llm/utils/sources.js +73 -0
- package/src/llm/utils/stream.js +75 -0
- package/src/llm/utils/tools.js +48 -0
- package/src/llm/utils/usage.js +156 -0
- package/src/llm/utils/usage_tools.js +23 -0
- package/src/llm/utils/wrap.js +277 -0
- package/src/llm/validations.js +234 -0
- package/src/sdk/helpers/path.d.ts +2 -0
- package/src/sdk/helpers/path.js +1 -0
- package/src/tracing/trace_attribute.d.ts +0 -17
- package/src/tracing/trace_attribute.js +1 -29
- package/src/worker/configs.js +23 -4
- package/src/worker/index.js +16 -47
- package/src/worker/interceptors/activity.js +0 -1
- package/src/worker/interceptors/workflow.js +0 -1
- package/src/worker/telemetry.js +122 -2
- package/src/worker/worker_options.js +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.15.1-next.
|
|
3
|
+
"version": "0.15.1-next.8ef04e2.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"./credentials": {
|
|
31
31
|
"types": "./dist/credentials/index.d.ts",
|
|
32
32
|
"import": "./dist/credentials/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./http": {
|
|
35
|
+
"types": "./src/http/index.d.ts",
|
|
36
|
+
"import": "./src/http/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./llm": {
|
|
39
|
+
"types": "./src/llm/index.d.ts",
|
|
40
|
+
"import": "./src/llm/index.js"
|
|
33
41
|
}
|
|
34
42
|
},
|
|
35
43
|
"files": [
|
|
@@ -37,7 +45,8 @@
|
|
|
37
45
|
"./dist",
|
|
38
46
|
"./bin",
|
|
39
47
|
"!**/*.spec.*",
|
|
40
|
-
"!src/credentials"
|
|
48
|
+
"!src/credentials",
|
|
49
|
+
"!src/llm/fixtures"
|
|
41
50
|
],
|
|
42
51
|
"bin": {
|
|
43
52
|
"output-worker": "./bin/worker.sh",
|
|
@@ -58,20 +67,61 @@
|
|
|
58
67
|
"@temporalio/common": "1.24.0",
|
|
59
68
|
"@temporalio/worker": "1.24.0",
|
|
60
69
|
"@temporalio/workflow": "1.24.0",
|
|
70
|
+
"decimal.js": "10.6.0",
|
|
71
|
+
"entities": "8.1.0",
|
|
61
72
|
"folder-hash": "4.1.3",
|
|
73
|
+
"gray-matter": "4.0.3",
|
|
62
74
|
"js-yaml": "5.4.2",
|
|
63
75
|
"json-stream-stringify": "3.1.7",
|
|
76
|
+
"liquidjs": "10.29.0",
|
|
64
77
|
"redis": "5.12.1",
|
|
65
78
|
"stacktrace-parser": "0.1.11",
|
|
66
|
-
"undici": "8.11.0",
|
|
67
79
|
"winston": "3.19.0"
|
|
68
80
|
},
|
|
69
81
|
"peerDependencies": {
|
|
70
|
-
"zod": ">=4.3 <5"
|
|
82
|
+
"zod": ">=4.3 <5",
|
|
83
|
+
"ky": ">=2 <3",
|
|
84
|
+
"undici": ">=8 <9",
|
|
85
|
+
"ai": ">=7 <8",
|
|
86
|
+
"@ai-sdk/amazon-bedrock": ">=5 <6",
|
|
87
|
+
"@ai-sdk/anthropic": ">=4 <5",
|
|
88
|
+
"@ai-sdk/azure": ">=4 <5",
|
|
89
|
+
"@ai-sdk/google-vertex": ">=5 <6",
|
|
90
|
+
"@ai-sdk/openai": ">=4 <5",
|
|
91
|
+
"@ai-sdk/perplexity": ">=4 <5"
|
|
92
|
+
},
|
|
93
|
+
"peerDependenciesMeta": {
|
|
94
|
+
"@ai-sdk/amazon-bedrock": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@ai-sdk/anthropic": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"@ai-sdk/azure": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"@ai-sdk/google-vertex": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"@ai-sdk/openai": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@ai-sdk/perplexity": {
|
|
110
|
+
"optional": true
|
|
111
|
+
}
|
|
71
112
|
},
|
|
72
113
|
"devDependencies": {
|
|
73
114
|
"@types/js-yaml": "4.0.9",
|
|
74
|
-
"zod": "4.6.5"
|
|
115
|
+
"zod": "4.6.5",
|
|
116
|
+
"ky": "2.0.2",
|
|
117
|
+
"undici": "8.10.2",
|
|
118
|
+
"ai": "7.0.112",
|
|
119
|
+
"@ai-sdk/amazon-bedrock": "5.0.92",
|
|
120
|
+
"@ai-sdk/anthropic": "4.0.61",
|
|
121
|
+
"@ai-sdk/azure": "4.0.77",
|
|
122
|
+
"@ai-sdk/google-vertex": "5.0.91",
|
|
123
|
+
"@ai-sdk/openai": "4.0.73",
|
|
124
|
+
"@ai-sdk/perplexity": "4.0.49"
|
|
75
125
|
},
|
|
76
126
|
"license": "Apache-2.0",
|
|
77
127
|
"publishConfig": {
|
|
@@ -83,6 +133,7 @@
|
|
|
83
133
|
"#credentials/*": "./dist/credentials/*.js",
|
|
84
134
|
"#errors": "./src/errors.js",
|
|
85
135
|
"#logger": "./src/logger/index.js",
|
|
136
|
+
"#runtime_logger": "./src/interface/logger.js",
|
|
86
137
|
"#hooks/*": "./src/hooks/*.js",
|
|
87
138
|
"#helpers/*": "./src/helpers/*.js",
|
|
88
139
|
"#tracing": "./src/tracing/internal_interface.js",
|
|
@@ -93,6 +144,7 @@
|
|
|
93
144
|
},
|
|
94
145
|
"scripts": {
|
|
95
146
|
"worker": "node ./src/worker/index.js",
|
|
96
|
-
"build": "rm -rf ./dist && tsc && pnpm exec tsc -p ./tsconfig.typecheck.json"
|
|
147
|
+
"build": "rm -rf ./dist && tsc && pnpm exec tsc -p ./tsconfig.typecheck.json",
|
|
148
|
+
"take-models-pricing-snapshot": "node ./scripts/take_models_pricing_snapshot.js"
|
|
97
149
|
}
|
|
98
150
|
}
|
package/src/helpers/fetch.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { FatalError } from '#errors';
|
|
2
2
|
import { redactHeaders } from './redact.js';
|
|
3
3
|
|
|
4
|
+
const jsonMatcher = /^application\/(?:json|[^;\s]+?\+json)(?:\s*;.*)?$/i;
|
|
5
|
+
const textMatcher = /^(?:text\/|application\/(?:xml|[^;\s]+?\+xml|x-www-form-urlencoded|javascript)(?:\s*;|$))/i;
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
|
-
* Consume the body of a Response according it its content-type and returns it
|
|
8
|
+
* Consume the body of a Response according it its content-type and returns it:
|
|
9
|
+
* - JSON content-type: parsed JSON, or the raw text if the body is empty or not valid JSON
|
|
10
|
+
* - text-like content-type: the text as-is
|
|
11
|
+
* - anything else: the body bytes as a base64 string
|
|
6
12
|
* @param {Response} response
|
|
7
|
-
* @returns {string|object|
|
|
13
|
+
* @returns {string|object|null} The response body content
|
|
8
14
|
*/
|
|
9
|
-
const consumeBody = async response => {
|
|
10
|
-
const
|
|
11
|
-
const contentType = ( headers['content-type'] ?? '' ).trim().toLowerCase();
|
|
12
|
-
const jsonMatcher = /^application\/(?:json|[^;\s]+?\+json)(?:\s*;.*)?$/i;
|
|
15
|
+
export const consumeBody = async response => {
|
|
16
|
+
const contentType = ( response.headers.get( 'content-type' ) ?? '' ).trim();
|
|
13
17
|
if ( jsonMatcher.test( contentType ) ) {
|
|
14
|
-
|
|
18
|
+
const text = await response.text();
|
|
19
|
+
if ( text.length === 0 ) {
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse( text );
|
|
24
|
+
} catch {
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
|
-
if (
|
|
28
|
+
if ( textMatcher.test( contentType ) ) {
|
|
17
29
|
return response.text();
|
|
18
30
|
}
|
|
19
31
|
return response.arrayBuffer().then( buf => Buffer.from( buf ).toString( 'base64' ) );
|
|
@@ -33,7 +45,7 @@ export const serializeResponse = async ( response, { includeHeaders = false, inc
|
|
|
33
45
|
status: response.status,
|
|
34
46
|
statusText: response.statusText,
|
|
35
47
|
ok: response.ok,
|
|
36
|
-
...( includeHeaders && { headers: redactHeaders(
|
|
48
|
+
...( includeHeaders && { headers: redactHeaders( response.headers ) } ),
|
|
37
49
|
...( includeBody && { body: await consumeBody( response ) } )
|
|
38
50
|
} );
|
|
39
51
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as stackTraceParser from 'stacktrace-parser';
|
|
2
|
+
|
|
3
|
+
// OS separator, but in a deterministic way, allowing this to work in Temporal's sandbox
|
|
4
|
+
// This avoids importing from node:path
|
|
5
|
+
const SEP = new Error().stack.includes( '/' ) ? '/' : '\\';
|
|
6
|
+
const transformSeparators = path => path.replaceAll( '/', SEP );
|
|
7
|
+
|
|
8
|
+
const defaultIgnorePaths = [
|
|
9
|
+
'/@outputai/core/',
|
|
10
|
+
'/@outputai/evals/',
|
|
11
|
+
'/sdk/core/',
|
|
12
|
+
'/sdk/evals/',
|
|
13
|
+
'node:internal/',
|
|
14
|
+
'evalmachine.',
|
|
15
|
+
'webpack/bootstrap'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the first immediate directory of the file invoking the code that called this function.
|
|
20
|
+
*
|
|
21
|
+
* Excludes `@outputai/core`, node, other internal paths, and any additional ignore paths.
|
|
22
|
+
*
|
|
23
|
+
* @param {string[]} [additionalIgnorePaths]
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export const resolveInvocationDir = ( additionalIgnorePaths = [] ) => {
|
|
27
|
+
const stack = new Error().stack;
|
|
28
|
+
const lines = stackTraceParser.parse( stack );
|
|
29
|
+
const ignorePaths = [ ...additionalIgnorePaths, ...defaultIgnorePaths ].map( transformSeparators );
|
|
30
|
+
|
|
31
|
+
const frame = lines.find( l => !ignorePaths.some( p => l.file.includes( p ) ) );
|
|
32
|
+
if ( !frame ) {
|
|
33
|
+
throw new Error( `Invocation dir resolution via stack trace failed. Stack: ${stack}` );
|
|
34
|
+
}
|
|
35
|
+
return frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
|
|
36
|
+
};
|
package/src/helpers/redact.js
CHANGED
|
@@ -43,7 +43,8 @@ export const redactHeaders = headers => {
|
|
|
43
43
|
wordEndMatcher( 'key' )
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
const plainHeaders = Object.fromEntries( headers );
|
|
47
|
+
return Object.entries( plainHeaders ).reduce( ( redacted, [ key, value ] ) => {
|
|
47
48
|
const lowKey = key.toLowerCase();
|
|
48
49
|
const isSensitive = !ignoreHeaders.has( lowKey ) && sensitiveHeadersPatterns.some( rx => rx.test( lowKey ) );
|
|
49
50
|
return Object.assign( redacted, { [key]: isSensitive ? '[REDACTED]' : value } );
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for HTTP client behavior
|
|
3
|
+
*/
|
|
4
|
+
export const config = {
|
|
5
|
+
/**
|
|
6
|
+
* Whether to log verbose HTTP information (headers and bodies)
|
|
7
|
+
* Controlled by OUTPUT_TRACE_HTTP_VERBOSE environment variable
|
|
8
|
+
*/
|
|
9
|
+
logVerbose: [ '1', 'true' ].includes( process.env.OUTPUT_TRACE_HTTP_VERBOSE )
|
|
10
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { KyResponse } from 'ky';
|
|
2
|
+
import { Attribute } from '#trace_attribute';
|
|
3
|
+
|
|
4
|
+
/** Trace attribute (`http:request:cost`) holding the cost attached to an HTTP request with `addRequestCost` */
|
|
5
|
+
export declare class HTTPRequestCost extends Attribute.BaseAttribute {
|
|
6
|
+
static TYPE: 'http:request:cost';
|
|
7
|
+
type: typeof HTTPRequestCost.TYPE;
|
|
8
|
+
url: string;
|
|
9
|
+
requestId: string;
|
|
10
|
+
total: number;
|
|
11
|
+
constructor( url: string, requestId: string, total: number );
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Attach cost information to the trace of an HTTP Request using the response
|
|
16
|
+
*
|
|
17
|
+
* @param response - The response of the HTTP Request to attach the information
|
|
18
|
+
* @param value - The price of the HTTP request
|
|
19
|
+
*/
|
|
20
|
+
export declare function addRequestCost( response: KyResponse | Response, value: number ): void;
|
package/src/http/cost.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { stepEventBus } from '#bus';
|
|
2
|
+
import { requestIdSymbol } from './request_tag.js';
|
|
3
|
+
import { Logger } from '#runtime_logger';
|
|
4
|
+
import { addEventActionWithContext, EventAction } from '#tracing';
|
|
5
|
+
import { BaseAttribute } from '#trace_attribute';
|
|
6
|
+
|
|
7
|
+
export class HTTPRequestCost extends BaseAttribute {
|
|
8
|
+
static TYPE = 'http:request:cost';
|
|
9
|
+
url;
|
|
10
|
+
requestId;
|
|
11
|
+
total = 0;
|
|
12
|
+
|
|
13
|
+
constructor( url, requestId, total ) {
|
|
14
|
+
super( HTTPRequestCost.TYPE );
|
|
15
|
+
this.url = url;
|
|
16
|
+
this.requestId = requestId;
|
|
17
|
+
this.total = total;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Attach cost information to the trace of an HTTP Request using the response
|
|
23
|
+
*
|
|
24
|
+
* @param {Response} response - The response of the HTTP Request to attach the information
|
|
25
|
+
* @param {number} value - The price of the HTTP request
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
export const addRequestCost = ( response, value ) => {
|
|
29
|
+
const eventId = Reflect.get( response, requestIdSymbol );
|
|
30
|
+
if ( !eventId ) {
|
|
31
|
+
Logger.warn(
|
|
32
|
+
'addRequestCost(): The "response" argument did not originate from @outputai/core/http, no costs were added.',
|
|
33
|
+
{ namespace: 'HTTP' }
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const attribute = new HTTPRequestCost( response.url, eventId, value );
|
|
39
|
+
addEventActionWithContext( EventAction.ADD_ATTR, { id: eventId, details: attribute } );
|
|
40
|
+
stepEventBus.emit( 'sdk:cost:http:request', attribute );
|
|
41
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { stepEventBus } from '#bus';
|
|
2
|
+
|
|
3
|
+
export const emitError = ( { requestId, method, url, status, durationMs } ) =>
|
|
4
|
+
stepEventBus.emit( 'sdk:http:request', { requestId, method, url, status, durationMs, outcome: 'error' } );
|
|
5
|
+
|
|
6
|
+
export const emitSuccess = ( { requestId, method, url, status, durationMs } ) =>
|
|
7
|
+
stepEventBus.emit( 'sdk:http:request', { requestId, method, url, status, durationMs, outcome: 'success' } );
|
|
8
|
+
|
|
9
|
+
export const emitFailure = ( { requestId, method, url, durationMs } ) =>
|
|
10
|
+
stepEventBus.emit( 'sdk:http:request', { requestId, method, url, status: undefined, durationMs, outcome: 'failure' } );
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type * as undici from 'undici';
|
|
2
|
+
|
|
3
|
+
type NodeRequestInfo = string | URL | globalThis.Request;
|
|
4
|
+
type NodeRequestInit = globalThis.RequestInit & Pick<undici.RequestInit, 'dispatcher'>;
|
|
5
|
+
|
|
6
|
+
type OutputFetch = {
|
|
7
|
+
( input: NodeRequestInfo, init?: NodeRequestInit ): Promise<Response>;
|
|
8
|
+
( input: undici.RequestInfo, init?: undici.RequestInit ): Promise<Response>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A fetch compliant function, that wraps undici's fetch.
|
|
13
|
+
*
|
|
14
|
+
* Behaves the same as any fetch function except:
|
|
15
|
+
* - Sets a request header called `x-request-trace-id` with a random UUID;
|
|
16
|
+
* - Sends the request, response, error and/or failure to the Trace system;
|
|
17
|
+
* - Emits a `http:request` event on every call (success, error, failure).
|
|
18
|
+
*
|
|
19
|
+
* Node and Undici realms can't be mixed: passing a Node `Request` with an Undici `FormData` body (or vice versa) throws a `TypeError`.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link https://fetch.spec.whatwg.org/}
|
|
22
|
+
* @param input - URL string, URL object or Request object (undici's or Node's)
|
|
23
|
+
* @param init - Request options
|
|
24
|
+
* @returns The HTTP response
|
|
25
|
+
*/
|
|
26
|
+
export declare const outputFetch: OutputFetch;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { logRequest, logResponse, logError, logFailure } from './logger.js';
|
|
3
|
+
import { emitSuccess, emitError, emitFailure } from './events.js';
|
|
4
|
+
import { addRequestIdToResponse } from '../request_tag.js';
|
|
5
|
+
import * as undici from 'undici';
|
|
6
|
+
|
|
7
|
+
/* Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299 */
|
|
8
|
+
const customDispatcher = new undici.EnvHttpProxyAgent( { allowH2: false } );
|
|
9
|
+
|
|
10
|
+
const createUndiciRequest = ( input, init ) => {
|
|
11
|
+
const isNodeRequest = input instanceof globalThis.Request;
|
|
12
|
+
const hasNodeFormData = init?.body instanceof globalThis.FormData;
|
|
13
|
+
const isUndiciRequest = input instanceof undici.Request;
|
|
14
|
+
const hasUndiciFormData = init?.body instanceof undici.FormData;
|
|
15
|
+
|
|
16
|
+
if ( ( isNodeRequest && hasUndiciFormData ) || ( isUndiciRequest && hasNodeFormData ) ) {
|
|
17
|
+
throw new TypeError( 'Cannot mix Node and Undici Request/FormData realms.' );
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if ( !isNodeRequest && !hasNodeFormData ) {
|
|
21
|
+
return new undici.Request( input, init );
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const request = new globalThis.Request( input, init );
|
|
25
|
+
return new undici.Request( request.url, request );
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A fetch compliant function, that wraps undici's fetch.
|
|
30
|
+
*
|
|
31
|
+
* @param {RequestInfo} input - URL string, URL object or Request object (undici's or Node's)
|
|
32
|
+
* @param {RequestInit} [init] - Request options
|
|
33
|
+
* @returns {Promise<Response>} The HTTP response
|
|
34
|
+
*/
|
|
35
|
+
export const outputFetch = async ( input, init ) => {
|
|
36
|
+
const { dispatcher: inputDispatcher, ...requestInit } = init ?? {};
|
|
37
|
+
|
|
38
|
+
// Creates a Request object with the many shapes RequestInfo can have
|
|
39
|
+
const base = createUndiciRequest( input, requestInit );
|
|
40
|
+
|
|
41
|
+
// Creates a headers object with the many shapes Request.Headers can have (object, array, Headers)
|
|
42
|
+
const headers = new undici.Headers( base.headers );
|
|
43
|
+
|
|
44
|
+
const requestId = randomUUID();
|
|
45
|
+
headers.set( 'x-request-trace-id', requestId );
|
|
46
|
+
const request = new undici.Request( base, { headers } );
|
|
47
|
+
|
|
48
|
+
const method = request.method;
|
|
49
|
+
const url = request.url;
|
|
50
|
+
const startedAt = Date.now();
|
|
51
|
+
|
|
52
|
+
await logRequest( { requestId, request } );
|
|
53
|
+
|
|
54
|
+
// this allows for users not only to override the dispatcher but also to define it as undefined and remove it altogether.
|
|
55
|
+
const dispatcher = Object.hasOwn( init ?? {}, 'dispatcher' ) ? inputDispatcher : customDispatcher;
|
|
56
|
+
try {
|
|
57
|
+
const response = await undici.fetch( request, dispatcher ? { dispatcher } : undefined );
|
|
58
|
+
const durationMs = Date.now() - startedAt;
|
|
59
|
+
const { status } = response;
|
|
60
|
+
|
|
61
|
+
// This enriches the response of the request id, so it is identifiable later.
|
|
62
|
+
addRequestIdToResponse( response, requestId );
|
|
63
|
+
|
|
64
|
+
if ( status > 399 ) {
|
|
65
|
+
await logError( { requestId, response } );
|
|
66
|
+
emitError( { requestId, method, url, status, durationMs } );
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
await logResponse( { requestId, response } );
|
|
70
|
+
emitSuccess( { requestId, method, url, status, durationMs } );
|
|
71
|
+
return response;
|
|
72
|
+
} catch ( error ) {
|
|
73
|
+
const durationMs = Date.now() - startedAt;
|
|
74
|
+
logFailure( { requestId, error } );
|
|
75
|
+
emitFailure( { requestId, method, url, durationMs } );
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Attribute } from '#trace_attribute';
|
|
2
|
+
|
|
3
|
+
/** Trace attribute (`http:request:count`) added to every HTTP request made with `outputFetch` or `createKyClient` */
|
|
4
|
+
export declare class HTTPRequestCount extends Attribute.BaseAttribute {
|
|
5
|
+
static TYPE: 'http:request:count';
|
|
6
|
+
type: typeof HTTPRequestCount.TYPE;
|
|
7
|
+
url: string;
|
|
8
|
+
requestId: string;
|
|
9
|
+
constructor( url: string, requestId: string );
|
|
10
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { addEventActionWithContext, EventAction } from '#tracing';
|
|
2
|
+
import { BaseAttribute } from '#trace_attribute';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import { redactHeaders } from '#helpers/redact';
|
|
5
|
+
import { consumeBody } from '#helpers/fetch';
|
|
6
|
+
|
|
7
|
+
export class HTTPRequestCount extends BaseAttribute {
|
|
8
|
+
static TYPE = 'http:request:count';
|
|
9
|
+
url;
|
|
10
|
+
requestId;
|
|
11
|
+
|
|
12
|
+
constructor( url, requestId ) {
|
|
13
|
+
super( HTTPRequestCount.TYPE );
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.requestId = requestId;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sends the trace start event for an http request
|
|
21
|
+
*
|
|
22
|
+
* @param options
|
|
23
|
+
* @param options.requestId - id of the request
|
|
24
|
+
* @param options.request - The HTTP Request object
|
|
25
|
+
*/
|
|
26
|
+
export const logRequest = async ( { requestId, request } ) => {
|
|
27
|
+
addEventActionWithContext( EventAction.START, {
|
|
28
|
+
id: requestId, kind: 'http', name: 'request', details: {
|
|
29
|
+
method: request.method,
|
|
30
|
+
url: request.url,
|
|
31
|
+
...( config.logVerbose && { headers: redactHeaders( request.headers ), body: await consumeBody( request.clone() ) } )
|
|
32
|
+
}
|
|
33
|
+
} );
|
|
34
|
+
addEventActionWithContext( EventAction.ADD_ATTR, { id: requestId, details: new HTTPRequestCount( request.url, requestId ) } );
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const logError = async ( { requestId, response } ) =>
|
|
38
|
+
addEventActionWithContext( EventAction.ERROR, {
|
|
39
|
+
id: requestId, details: {
|
|
40
|
+
status: response.status,
|
|
41
|
+
statusText: response.statusText,
|
|
42
|
+
...( config.logVerbose && { headers: redactHeaders( response.headers ), body: await consumeBody( response.clone() ) } )
|
|
43
|
+
}
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
export const logResponse = async ( { requestId, response } ) =>
|
|
47
|
+
addEventActionWithContext( EventAction.END, {
|
|
48
|
+
id: requestId, details: {
|
|
49
|
+
status: response.status,
|
|
50
|
+
statusText: response.statusText,
|
|
51
|
+
...( config.logVerbose && { headers: redactHeaders( response.headers ), body: await consumeBody( response.clone() ) } )
|
|
52
|
+
}
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
export const logFailure = ( { requestId, error } ) =>
|
|
56
|
+
addEventActionWithContext( EventAction.ERROR, { id: requestId, details: error } );
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Payload of the `http:request` event */
|
|
2
|
+
export type HttpRequestEvent = {
|
|
3
|
+
requestId: string;
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
status: number | undefined;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
outcome: 'success' | 'error' | 'failure';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Payload of the `cost:http:request` event */
|
|
12
|
+
export type HttpRequestCostEvent = {
|
|
13
|
+
type: 'http:request:cost';
|
|
14
|
+
requestId: string;
|
|
15
|
+
url: string;
|
|
16
|
+
total: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { outputFetch } from './fetch/index.js';
|
|
20
|
+
export { HTTPRequestCount } from './fetch/logger.js';
|
|
21
|
+
export { createKyClient } from './ky/index.js';
|
|
22
|
+
|
|
23
|
+
export { addRequestCost, HTTPRequestCost } from './cost.js';
|
|
24
|
+
|
|
25
|
+
/** Re-export ky library for convenience. */
|
|
26
|
+
export * as ky from 'ky';
|
|
27
|
+
export * as undici from 'undici';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { outputFetch } from './fetch/index.js';
|
|
2
|
+
export { HTTPRequestCount } from './fetch/logger.js';
|
|
3
|
+
export { createKyClient } from './ky/index.js';
|
|
4
|
+
|
|
5
|
+
export { addRequestCost, HTTPRequestCost } from './cost.js';
|
|
6
|
+
|
|
7
|
+
/** Re-export ky library for convenience. */
|
|
8
|
+
export * as ky from 'ky';
|
|
9
|
+
export * as undici from 'undici';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KyInstance, Options } from 'ky';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a ky client.
|
|
5
|
+
*
|
|
6
|
+
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createKyClient } from '@outputai/core/http';
|
|
11
|
+
*
|
|
12
|
+
* const client = createKyClient({
|
|
13
|
+
* prefix: 'https://api.example.com',
|
|
14
|
+
* timeout: 30000,
|
|
15
|
+
* retry: { limit: 3 }
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const response = await client.get('users/1');
|
|
19
|
+
* const data = await response.json();
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @param options - The ky options to extend the base client.
|
|
23
|
+
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createKyClient( options?: Options ): KyInstance;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
import { outputFetch } from '../fetch/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a ky client that uses `outputFetch`.
|
|
6
|
+
*
|
|
7
|
+
* @param {import('ky').Options} options - The ky options to extend the base client.
|
|
8
|
+
* @returns {import('ky').KyInstance} A ky instance extended with Output.ai tracing hooks.
|
|
9
|
+
*/
|
|
10
|
+
export const createKyClient = ( options = {} ) => ky.create( { fetch: outputFetch, ...options } );
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol used to store request id in the response object.
|
|
3
|
+
*/
|
|
4
|
+
export const requestIdSymbol = Symbol( 'request_id' );
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tag a response in place with its request id so downstream code (e.g.
|
|
8
|
+
* `addRequestCost`) can correlate. Stores the id under a private symbol AND
|
|
9
|
+
* patches `clone()` so the tag propagates to clones — ky clones the response
|
|
10
|
+
* before invoking `afterResponse` hooks, and undici headers are immutable on
|
|
11
|
+
* received responses, so a symbol re-attached inside `clone()` is the only
|
|
12
|
+
* path that survives.
|
|
13
|
+
*
|
|
14
|
+
* @param {Response} response
|
|
15
|
+
* @param {string} requestId
|
|
16
|
+
*/
|
|
17
|
+
export const addRequestIdToResponse = ( response, requestId ) => {
|
|
18
|
+
Object.defineProperty( response, requestIdSymbol, { value: requestId, enumerable: false, configurable: false, writable: false } );
|
|
19
|
+
const originalClone = response.clone.bind( response );
|
|
20
|
+
Object.defineProperty( response, 'clone', {
|
|
21
|
+
value: function clone() {
|
|
22
|
+
const cloned = originalClone();
|
|
23
|
+
addRequestIdToResponse( cloned, requestId );
|
|
24
|
+
return cloned;
|
|
25
|
+
},
|
|
26
|
+
enumerable: false,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true
|
|
29
|
+
} );
|
|
30
|
+
};
|