@outputai/core 0.15.1-next.4a437bf.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.
Files changed (62) hide show
  1. package/package.json +58 -6
  2. package/src/helpers/fetch.js +21 -9
  3. package/src/helpers/path.js +36 -0
  4. package/src/helpers/redact.js +2 -1
  5. package/src/http/config.js +10 -0
  6. package/src/http/cost.d.ts +20 -0
  7. package/src/http/cost.js +41 -0
  8. package/src/http/fetch/events.js +10 -0
  9. package/src/http/fetch/index.d.ts +26 -0
  10. package/src/http/fetch/index.js +78 -0
  11. package/src/http/fetch/logger.d.ts +10 -0
  12. package/src/http/fetch/logger.js +56 -0
  13. package/src/http/index.d.ts +27 -0
  14. package/src/http/index.js +9 -0
  15. package/src/http/ky/index.d.ts +25 -0
  16. package/src/http/ky/index.js +10 -0
  17. package/src/http/request_tag.js +30 -0
  18. package/src/llm/agent.js +141 -0
  19. package/src/llm/ai_provider.js +131 -0
  20. package/src/llm/ai_sdk_options.js +126 -0
  21. package/src/llm/consts.js +6 -0
  22. package/src/llm/deprecated_provider_aliases.js +8 -0
  23. package/src/llm/generate.js +88 -0
  24. package/src/llm/index.d.ts +631 -0
  25. package/src/llm/index.js +5 -0
  26. package/src/llm/prompt/content.js +70 -0
  27. package/src/llm/prompt/interpolations.js +58 -0
  28. package/src/llm/prompt/loader.js +67 -0
  29. package/src/llm/prompt/markup/attributes.js +52 -0
  30. package/src/llm/prompt/markup/nodes.js +71 -0
  31. package/src/llm/prompt/markup/tokenizer.js +3 -0
  32. package/src/llm/prompt/markup/tokens.js +26 -0
  33. package/src/llm/prompt/validations.js +129 -0
  34. package/src/llm/utils/cost.js +150 -0
  35. package/src/llm/utils/error_handler.js +53 -0
  36. package/src/llm/utils/file.js +42 -0
  37. package/src/llm/utils/google_vertex_auth.js +31 -0
  38. package/src/llm/utils/grounding.js +49 -0
  39. package/src/llm/utils/image.js +37 -0
  40. package/src/llm/utils/legacy_cost_attribute.js +75 -0
  41. package/src/llm/utils/metering.js +126 -0
  42. package/src/llm/utils/models.js +15 -0
  43. package/src/llm/utils/models_pricing.js +93 -0
  44. package/src/llm/utils/models_pricing_snapshot.json +4166 -0
  45. package/src/llm/utils/skills.js +74 -0
  46. package/src/llm/utils/sources.js +73 -0
  47. package/src/llm/utils/stream.js +75 -0
  48. package/src/llm/utils/tools.js +48 -0
  49. package/src/llm/utils/usage.js +156 -0
  50. package/src/llm/utils/usage_tools.js +23 -0
  51. package/src/llm/utils/wrap.js +277 -0
  52. package/src/llm/validations.js +234 -0
  53. package/src/sdk/helpers/path.d.ts +2 -0
  54. package/src/sdk/helpers/path.js +1 -0
  55. package/src/tracing/trace_attribute.d.ts +0 -17
  56. package/src/tracing/trace_attribute.js +1 -29
  57. package/src/worker/configs.js +23 -4
  58. package/src/worker/index.js +16 -47
  59. package/src/worker/interceptors/activity.js +0 -1
  60. package/src/worker/interceptors/workflow.js +0 -1
  61. package/src/worker/telemetry.js +122 -2
  62. 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.4a437bf.0",
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
  }
@@ -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|undefined|null} The response body content
13
+ * @returns {string|object|null} The response body content
8
14
  */
9
- const consumeBody = async response => {
10
- const headers = Object.fromEntries( response.headers ) ?? {};
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
- return response.json();
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 ( contentType.startsWith( 'text/' ) ) {
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( Object.fromEntries( response.headers ) ) } ),
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
+ };
@@ -43,7 +43,8 @@ export const redactHeaders = headers => {
43
43
  wordEndMatcher( 'key' )
44
44
  ];
45
45
 
46
- return Object.entries( headers ).reduce( ( redacted, [ key, value ] ) => {
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;
@@ -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
+ };