@outputai/core 0.10.1-next.f6a7c1a.0 → 0.11.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 +13 -7
- package/src/consts.js +5 -2
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +55 -13
- package/src/helpers/errors.spec.js +166 -66
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +35 -44
- package/src/hooks/index.js +10 -7
- package/src/hooks/index.spec.js +34 -7
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/index.js +2 -1
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +24 -52
- package/src/interface/workflow.spec.js +109 -108
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/index.js +21 -8
- package/src/worker/index.spec.js +30 -10
- package/src/worker/interceptors/activity.js +32 -19
- package/src/worker/interceptors/activity.spec.js +71 -166
- package/src/worker/interceptors/workflow.js +53 -11
- package/src/worker/interceptors/workflow.spec.js +176 -49
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +37 -5
- package/src/worker/log_hooks.js +13 -10
- package/src/worker/log_hooks.spec.js +12 -4
- package/src/worker/sinks.js +3 -1
- package/src/worker/sinks.spec.js +21 -4
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
|
@@ -27,9 +27,14 @@ const mainEventBusMock = vi.hoisted( () => ( {
|
|
|
27
27
|
onHandlers[eventType] = handler;
|
|
28
28
|
} )
|
|
29
29
|
} ) );
|
|
30
|
+
const serializeErrorMock = vi.hoisted( () => vi.fn( error => ( {
|
|
31
|
+
name: error.name,
|
|
32
|
+
message: error.message
|
|
33
|
+
} ) ) );
|
|
30
34
|
|
|
31
35
|
vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
|
|
32
36
|
vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
|
|
37
|
+
vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
|
|
33
38
|
|
|
34
39
|
import './log_hooks.js';
|
|
35
40
|
|
|
@@ -104,6 +109,7 @@ describe( 'log_hooks', () => {
|
|
|
104
109
|
|
|
105
110
|
it( 'ACTIVITY_ERROR logs full message and second arg', () => {
|
|
106
111
|
const err = new Error( 'step failed' );
|
|
112
|
+
err.name = 'ProviderError';
|
|
107
113
|
onHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
108
114
|
...basePayload,
|
|
109
115
|
duration: 100,
|
|
@@ -112,7 +118,7 @@ describe( 'log_hooks', () => {
|
|
|
112
118
|
|
|
113
119
|
expect( activityLogMock.error ).toHaveBeenCalledTimes( 1 );
|
|
114
120
|
expect( activityLogMock.error ).toHaveBeenCalledWith(
|
|
115
|
-
'Error myWorkflow#myStep step:
|
|
121
|
+
'Error myWorkflow#myStep step: ProviderError',
|
|
116
122
|
{
|
|
117
123
|
event: LifecycleEvent.ERROR,
|
|
118
124
|
activityId: 'act-1',
|
|
@@ -120,9 +126,10 @@ describe( 'log_hooks', () => {
|
|
|
120
126
|
workflowId: 'wf-1',
|
|
121
127
|
workflowType: 'myWorkflow',
|
|
122
128
|
runId: 'run-1',
|
|
123
|
-
error: 'step failed'
|
|
129
|
+
error: { name: 'ProviderError', message: 'step failed' }
|
|
124
130
|
}
|
|
125
131
|
);
|
|
132
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( err, { dropKeys: [ 'stack' ] } );
|
|
126
133
|
} );
|
|
127
134
|
|
|
128
135
|
it( 'ACTIVITY_ERROR does not log trace destination activity', () => {
|
|
@@ -229,7 +236,7 @@ describe( 'log_hooks', () => {
|
|
|
229
236
|
} );
|
|
230
237
|
|
|
231
238
|
it( 'WORKFLOW_ERROR logs full message and second arg', () => {
|
|
232
|
-
const err =
|
|
239
|
+
const err = { name: 'TypeError', message: 'workflow boom' };
|
|
233
240
|
onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
234
241
|
...basePayload,
|
|
235
242
|
duration: 150,
|
|
@@ -244,9 +251,10 @@ describe( 'log_hooks', () => {
|
|
|
244
251
|
workflowId: 'wf-1',
|
|
245
252
|
workflowType: 'myWorkflow',
|
|
246
253
|
runId: 'run-1',
|
|
247
|
-
error: 'workflow boom'
|
|
254
|
+
error: { name: 'TypeError', message: 'workflow boom' }
|
|
248
255
|
}
|
|
249
256
|
);
|
|
257
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( err, { dropKeys: [ 'stack' ] } );
|
|
250
258
|
} );
|
|
251
259
|
|
|
252
260
|
it( 'WORKFLOW_ERROR does not log when workflowType is WORKFLOW_CATALOG', () => {
|
package/src/worker/sinks.js
CHANGED
|
@@ -2,6 +2,7 @@ import { BusEventType, ComponentType } from '#consts';
|
|
|
2
2
|
import * as Tracing from '#tracing';
|
|
3
3
|
import { mainEventBus } from '#bus';
|
|
4
4
|
import { createWorkflowDetails } from '#helpers/temporal_context';
|
|
5
|
+
import { rehydrateError } from '#helpers/errors';
|
|
5
6
|
|
|
6
7
|
// This sink allow for sandbox Temporal environment to send trace logs back to the main thread.
|
|
7
8
|
export const sinks = {
|
|
@@ -48,7 +49,8 @@ export const sinks = {
|
|
|
48
49
|
error: {
|
|
49
50
|
fn: ( workflowInfo, error ) => {
|
|
50
51
|
const { runId, memo: { traceInfo } } = workflowInfo;
|
|
51
|
-
|
|
52
|
+
// re-hydrate workflow error
|
|
53
|
+
mainEventBus.emit( BusEventType.WORKFLOW_ERROR, { workflowDetails: createWorkflowDetails( workflowInfo ), error: rehydrateError( error ) } );
|
|
52
54
|
if ( traceInfo ) {
|
|
53
55
|
Tracing.addEventError( { id: runId, details: error, traceInfo } );
|
|
54
56
|
}
|
package/src/worker/sinks.spec.js
CHANGED
|
@@ -15,6 +15,8 @@ const createWorkflowDetailsMock = vi.hoisted( () => vi.fn( workflowInfo => ( {
|
|
|
15
15
|
runId: workflowInfo.runId
|
|
16
16
|
} ) ) );
|
|
17
17
|
|
|
18
|
+
const rehydrateErrorMock = vi.hoisted( () => vi.fn( error => error ) );
|
|
19
|
+
|
|
18
20
|
vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
|
|
19
21
|
vi.mock( '#tracing', () => ( {
|
|
20
22
|
addEventStart: addEventStartMock,
|
|
@@ -24,6 +26,9 @@ vi.mock( '#tracing', () => ( {
|
|
|
24
26
|
vi.mock( '#helpers/temporal_context', () => ( {
|
|
25
27
|
createWorkflowDetails: createWorkflowDetailsMock
|
|
26
28
|
} ) );
|
|
29
|
+
vi.mock( '#helpers/errors', () => ( {
|
|
30
|
+
rehydrateError: rehydrateErrorMock
|
|
31
|
+
} ) );
|
|
27
32
|
|
|
28
33
|
describe( 'worker/sinks', () => {
|
|
29
34
|
const workflowInfo = {
|
|
@@ -128,11 +133,17 @@ describe( 'worker/sinks', () => {
|
|
|
128
133
|
|
|
129
134
|
it( 'workflow.error emits workflow error events and trace error events', async () => {
|
|
130
135
|
const { sinks } = await import( './sinks.js' );
|
|
131
|
-
const error =
|
|
136
|
+
const error = { name: 'Error', message: 'workflow failed' };
|
|
137
|
+
const rehydrated = new Error( 'workflow failed' );
|
|
138
|
+
rehydrateErrorMock.mockReturnValueOnce( rehydrated );
|
|
132
139
|
|
|
133
140
|
sinks.workflow.error.fn( workflowInfo, error );
|
|
134
141
|
|
|
135
|
-
expect(
|
|
142
|
+
expect( rehydrateErrorMock ).toHaveBeenCalledWith( error );
|
|
143
|
+
expect( mainEventBusMock.emit ).toHaveBeenCalledWith(
|
|
144
|
+
BusEventType.WORKFLOW_ERROR,
|
|
145
|
+
{ workflowDetails, error: rehydrated }
|
|
146
|
+
);
|
|
136
147
|
expect( addEventErrorMock ).toHaveBeenCalledWith( {
|
|
137
148
|
id: 'run-1',
|
|
138
149
|
details: error,
|
|
@@ -142,11 +153,17 @@ describe( 'worker/sinks', () => {
|
|
|
142
153
|
|
|
143
154
|
it( 'workflow.error skips tracing when trace info is absent', async () => {
|
|
144
155
|
const { sinks } = await import( './sinks.js' );
|
|
145
|
-
const error =
|
|
156
|
+
const error = { name: 'Error', message: 'workflow failed' };
|
|
157
|
+
const rehydrated = new Error( 'workflow failed' );
|
|
158
|
+
rehydrateErrorMock.mockReturnValueOnce( rehydrated );
|
|
146
159
|
|
|
147
160
|
sinks.workflow.error.fn( { ...workflowInfo, memo: {} }, error );
|
|
148
161
|
|
|
149
|
-
expect(
|
|
162
|
+
expect( rehydrateErrorMock ).toHaveBeenCalledWith( error );
|
|
163
|
+
expect( mainEventBusMock.emit ).toHaveBeenCalledWith(
|
|
164
|
+
BusEventType.WORKFLOW_ERROR,
|
|
165
|
+
{ workflowDetails, error: rehydrated }
|
|
166
|
+
);
|
|
150
167
|
expect( addEventErrorMock ).not.toHaveBeenCalled();
|
|
151
168
|
} );
|
|
152
169
|
|
package/src/worker/telemetry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serializeError } from '#helpers/error_serializer';
|
|
1
2
|
import { createChildLogger } from '#logger';
|
|
2
3
|
import { workerTelemetryIntervalMs } from './configs.js';
|
|
3
4
|
|
|
@@ -18,7 +19,7 @@ export const setupTelemetry = ( { worker } ) => {
|
|
|
18
19
|
}
|
|
19
20
|
} );
|
|
20
21
|
} catch ( error ) {
|
|
21
|
-
log.warn( 'Failure', { error: error
|
|
22
|
+
log.warn( 'Failure', { error: serializeError( error, { dropKeys: [ 'stack' ] } ) } );
|
|
22
23
|
}
|
|
23
24
|
}, workerTelemetryIntervalMs ).unref();
|
|
24
25
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DefaultLogger, makeTelemetryFilterString, Runtime } from '@temporalio/worker';
|
|
2
|
+
import { createChildLogger } from '#logger';
|
|
3
|
+
|
|
4
|
+
const log = createChildLogger( 'Temporal' );
|
|
5
|
+
const OMITTED_MESSAGES = [ 'Activity failed', 'Workflow failed' ];
|
|
6
|
+
|
|
7
|
+
const temporalLevelToInternalLevelMap = {
|
|
8
|
+
TRACE: 'debug',
|
|
9
|
+
DEBUG: 'debug',
|
|
10
|
+
INFO: 'info',
|
|
11
|
+
WARN: 'warn',
|
|
12
|
+
ERROR: 'error'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const setupTemporalLogger = () => {
|
|
16
|
+
const level = process.env.OUTPUT_TEMPORAL_LOG_LEVEL?.toUpperCase() ?? 'INFO';
|
|
17
|
+
|
|
18
|
+
Runtime.install( {
|
|
19
|
+
logger: new DefaultLogger( level, entry => {
|
|
20
|
+
// Already reported by Output activity logs and traces.
|
|
21
|
+
if ( OMITTED_MESSAGES.includes( entry.message ) ) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
log.log( temporalLevelToInternalLevelMap[entry.level], entry.message, entry.meta );
|
|
25
|
+
} ),
|
|
26
|
+
telemetryOptions: {
|
|
27
|
+
logging: {
|
|
28
|
+
forward: {},
|
|
29
|
+
filter: makeTelemetryFilterString( { core: level, other: level } )
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} );
|
|
33
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
defaultLoggerMock,
|
|
5
|
+
logMock,
|
|
6
|
+
makeTelemetryFilterStringMock,
|
|
7
|
+
runtimeInstallMock
|
|
8
|
+
} = vi.hoisted( () => ( {
|
|
9
|
+
defaultLoggerMock: vi.fn( function ( level, callback ) {
|
|
10
|
+
this.level = level;
|
|
11
|
+
this.callback = callback;
|
|
12
|
+
} ),
|
|
13
|
+
logMock: { log: vi.fn() },
|
|
14
|
+
makeTelemetryFilterStringMock: vi.fn().mockReturnValue( 'temporal-filter' ),
|
|
15
|
+
runtimeInstallMock: vi.fn()
|
|
16
|
+
} ) );
|
|
17
|
+
|
|
18
|
+
vi.mock( '@temporalio/worker', () => ( {
|
|
19
|
+
DefaultLogger: defaultLoggerMock,
|
|
20
|
+
makeTelemetryFilterString: makeTelemetryFilterStringMock,
|
|
21
|
+
Runtime: { install: runtimeInstallMock }
|
|
22
|
+
} ) );
|
|
23
|
+
vi.mock( '#logger', () => ( { createChildLogger: () => logMock } ) );
|
|
24
|
+
|
|
25
|
+
import { setupTemporalLogger } from './temporal_logger.js';
|
|
26
|
+
|
|
27
|
+
describe( 'setupTemporalLogger', () => {
|
|
28
|
+
beforeEach( () => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
vi.stubEnv( 'OUTPUT_TEMPORAL_LOG_LEVEL', undefined );
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
afterEach( () => {
|
|
34
|
+
vi.unstubAllEnvs();
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'forwards Temporal SDK and Core logs through the application logger', () => {
|
|
38
|
+
setupTemporalLogger();
|
|
39
|
+
|
|
40
|
+
expect( makeTelemetryFilterStringMock ).toHaveBeenCalledWith( { core: 'INFO', other: 'INFO' } );
|
|
41
|
+
expect( defaultLoggerMock ).toHaveBeenCalledWith( 'INFO', expect.any( Function ) );
|
|
42
|
+
expect( runtimeInstallMock ).toHaveBeenCalledWith( {
|
|
43
|
+
logger: expect.any( defaultLoggerMock ),
|
|
44
|
+
telemetryOptions: {
|
|
45
|
+
logging: {
|
|
46
|
+
forward: {},
|
|
47
|
+
filter: 'temporal-filter'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
|
|
53
|
+
entryCallback( { level: 'WARN', message: 'Worker warning', meta: { taskQueue: 'default' } } );
|
|
54
|
+
|
|
55
|
+
expect( logMock.log ).toHaveBeenCalledWith( 'warn', 'Worker warning', { taskQueue: 'default' } );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it.each( [
|
|
59
|
+
[ 'TRACE', 'debug' ],
|
|
60
|
+
[ 'DEBUG', 'debug' ],
|
|
61
|
+
[ 'INFO', 'info' ],
|
|
62
|
+
[ 'WARN', 'warn' ],
|
|
63
|
+
[ 'ERROR', 'error' ]
|
|
64
|
+
] )( 'maps Temporal %s logs to the %s application level', ( temporalLevel, internalLevel ) => {
|
|
65
|
+
setupTemporalLogger();
|
|
66
|
+
const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
|
|
67
|
+
|
|
68
|
+
entryCallback( { level: temporalLevel, message: `${temporalLevel} event`, meta: { source: 'test' } } );
|
|
69
|
+
|
|
70
|
+
expect( logMock.log ).toHaveBeenCalledWith( internalLevel, `${temporalLevel} event`, { source: 'test' } );
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
it( 'applies a case-insensitive configured log level to SDK and Core logs', () => {
|
|
74
|
+
vi.stubEnv( 'OUTPUT_TEMPORAL_LOG_LEVEL', 'debug' );
|
|
75
|
+
|
|
76
|
+
setupTemporalLogger();
|
|
77
|
+
|
|
78
|
+
expect( defaultLoggerMock ).toHaveBeenCalledWith( 'DEBUG', expect.any( Function ) );
|
|
79
|
+
expect( makeTelemetryFilterStringMock ).toHaveBeenCalledWith( { core: 'DEBUG', other: 'DEBUG' } );
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
it.each( [ 'Activity failed', 'Workflow failed' ] )( 'omits redundant "%s" events', message => {
|
|
83
|
+
setupTemporalLogger();
|
|
84
|
+
const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
|
|
85
|
+
|
|
86
|
+
entryCallback( { level: 'WARN', message, meta: { error: new Error( 'failed' ) } } );
|
|
87
|
+
|
|
88
|
+
expect( logMock.log ).not.toHaveBeenCalled();
|
|
89
|
+
} );
|
|
90
|
+
} );
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Attribute } from '#trace_attribute';
|
|
2
|
-
import Decimal from 'decimal.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {object} Aggregation
|
|
6
|
-
*
|
|
7
|
-
* @property {object} cost
|
|
8
|
-
* @property {object} cost.total - Total cost
|
|
9
|
-
* @property {object} tokens
|
|
10
|
-
* @property {object} tokens.total - Total tokens used
|
|
11
|
-
* @property {object} tokens.input - Total input tokens used
|
|
12
|
-
* @property {object} tokens.input_cached - Total cached input tokens used
|
|
13
|
-
* @property {object} tokens.output - Total output tokens used
|
|
14
|
-
* @property {object} tokens.reasoning - Total reasoning tokens used
|
|
15
|
-
* @property {object} httpRequests
|
|
16
|
-
* @property {object} httpRequests.total - Total number of http requests made
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Aggregates a collection of Attributes into a object with totals
|
|
20
|
-
*
|
|
21
|
-
* @param {Attribute} attributes
|
|
22
|
-
* @returns {Aggregation} aggregation object
|
|
23
|
-
*/
|
|
24
|
-
export const aggregateAttributes = attributes => ( {
|
|
25
|
-
cost: {
|
|
26
|
-
total: attributes
|
|
27
|
-
.filter( a => [ Attribute.HTTPRequestCost.TYPE, Attribute.LLMUsage.TYPE ].includes( a.type ) )
|
|
28
|
-
.reduce( ( sum, a ) => sum.add( a.total ), Decimal( 0 ) ).toNumber()
|
|
29
|
-
},
|
|
30
|
-
tokens: {
|
|
31
|
-
total: attributes
|
|
32
|
-
.filter( a => Attribute.LLMUsage.TYPE === a.type )
|
|
33
|
-
.reduce( ( sum, a ) => sum.add( a.tokensUsed ), Decimal( 0 ) ).toNumber(),
|
|
34
|
-
...Object.entries( attributes
|
|
35
|
-
.filter( a => Attribute.LLMUsage.TYPE === a.type )
|
|
36
|
-
.flatMap( a => a.usage )
|
|
37
|
-
.reduce( ( obj, a ) => Object.assign( obj, { [a.type]: ( obj[a.type] ?? Decimal( 0 ) ).add( a.amount ) } ), {} ) )
|
|
38
|
-
.reduce( ( obj, [ k, v ] ) => Object.assign( obj, { [k]: v.toNumber() } ), {} ) // convert all values to number
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
httpRequests: {
|
|
42
|
-
total: attributes.filter( a => Attribute.HTTPRequestCount.TYPE === a.type ).length
|
|
43
|
-
}
|
|
44
|
-
} );
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { Attribute } from '#trace_attribute';
|
|
3
|
-
import { aggregateAttributes } from './aggregations.js';
|
|
4
|
-
|
|
5
|
-
describe( 'aggregateAttributes', () => {
|
|
6
|
-
it( 'returns zeroed aggregations when there are no attributes', () => {
|
|
7
|
-
expect( aggregateAttributes( [] ) ).toEqual( {
|
|
8
|
-
cost: { total: 0 },
|
|
9
|
-
tokens: { total: 0 },
|
|
10
|
-
httpRequests: { total: 0 }
|
|
11
|
-
} );
|
|
12
|
-
} );
|
|
13
|
-
|
|
14
|
-
it( 'aggregates costs, token usage, and HTTP request count by attribute type', () => {
|
|
15
|
-
const attributes = [
|
|
16
|
-
{
|
|
17
|
-
type: Attribute.HTTPRequestCount.TYPE,
|
|
18
|
-
url: 'https://api.example.test/a',
|
|
19
|
-
requestId: 'req-1'
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
type: Attribute.HTTPRequestCount.TYPE,
|
|
23
|
-
url: 'https://api.example.test/b',
|
|
24
|
-
requestId: 'req-2'
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
type: Attribute.HTTPRequestCost.TYPE,
|
|
28
|
-
url: 'https://api.example.test/a',
|
|
29
|
-
requestId: 'req-1',
|
|
30
|
-
total: 0.2
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
type: Attribute.LLMUsage.TYPE,
|
|
34
|
-
modelId: 'gpt-4o',
|
|
35
|
-
total: 0.3,
|
|
36
|
-
tokensUsed: 120,
|
|
37
|
-
usage: [
|
|
38
|
-
{ type: 'input', ppm: 1, amount: 100, total: 0.1 },
|
|
39
|
-
{ type: 'output', ppm: 2, amount: 20, total: 0.2 }
|
|
40
|
-
]
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
type: Attribute.LLMUsage.TYPE,
|
|
44
|
-
modelId: 'gpt-4o-mini',
|
|
45
|
-
total: 0.05,
|
|
46
|
-
tokensUsed: 30,
|
|
47
|
-
usage: [
|
|
48
|
-
{ type: 'input', ppm: 1, amount: 25, total: 0.025 },
|
|
49
|
-
{ type: 'reasoning', ppm: 5, amount: 5, total: 0.025 }
|
|
50
|
-
]
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
type: 'unrelated',
|
|
54
|
-
total: 100,
|
|
55
|
-
tokensUsed: 100
|
|
56
|
-
}
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
expect( aggregateAttributes( attributes ) ).toEqual( {
|
|
60
|
-
cost: { total: 0.55 },
|
|
61
|
-
tokens: {
|
|
62
|
-
total: 150,
|
|
63
|
-
input: 125,
|
|
64
|
-
output: 20,
|
|
65
|
-
reasoning: 5
|
|
66
|
-
},
|
|
67
|
-
httpRequests: { total: 2 }
|
|
68
|
-
} );
|
|
69
|
-
} );
|
|
70
|
-
|
|
71
|
-
it( 'uses LLMUsage.tokensUsed for total tokens instead of summing usage amounts', () => {
|
|
72
|
-
const attributes = [
|
|
73
|
-
{
|
|
74
|
-
type: Attribute.LLMUsage.TYPE,
|
|
75
|
-
modelId: 'provider-model',
|
|
76
|
-
total: 0.1,
|
|
77
|
-
tokensUsed: 42,
|
|
78
|
-
usage: [
|
|
79
|
-
{ type: 'input', ppm: 1, amount: 10, total: 0.01 },
|
|
80
|
-
{ type: 'output', ppm: 1, amount: 5, total: 0.005 }
|
|
81
|
-
]
|
|
82
|
-
}
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
expect( aggregateAttributes( attributes ).tokens ).toEqual( {
|
|
86
|
-
total: 42,
|
|
87
|
-
input: 10,
|
|
88
|
-
output: 5
|
|
89
|
-
} );
|
|
90
|
-
} );
|
|
91
|
-
} );
|
|
92
|
-
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { inspect } from 'node:util';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {object} SerializedError
|
|
5
|
-
* @property {string} name - The error constructor name
|
|
6
|
-
* @property {string} message - The error message
|
|
7
|
-
* @property {string} stack - The error stack trace
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Recursively Serialize an error object. Navigate using "cause" property.
|
|
12
|
-
*
|
|
13
|
-
* Goes up 10 levels deep.
|
|
14
|
-
*
|
|
15
|
-
* @param {Error} error
|
|
16
|
-
* @returns {SerializedError}
|
|
17
|
-
*/
|
|
18
|
-
export const serializeError = ( () => {
|
|
19
|
-
|
|
20
|
-
const serializeValue = v => {
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse( JSON.stringify( v ) );
|
|
23
|
-
} catch {
|
|
24
|
-
return inspect( v, { depth: 5, breakLength: Infinity, colors: false } );
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
return ( error, depth = 0 ) => {
|
|
29
|
-
if ( depth > 10 ) {
|
|
30
|
-
return { name: 'Error', message: 'Cause chain too deep' };
|
|
31
|
-
}
|
|
32
|
-
if ( error instanceof Error ) {
|
|
33
|
-
return {
|
|
34
|
-
name: error.constructor.name,
|
|
35
|
-
message: error.message,
|
|
36
|
-
stack: error.stack,
|
|
37
|
-
...( error.cause !== undefined ? { cause: serializeError( error.cause, depth + 1 ) } : {} )
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
return serializeValue( error );
|
|
41
|
-
};
|
|
42
|
-
} )();
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { serializeError } from './utils.js';
|
|
3
|
-
|
|
4
|
-
describe( 'serializeError', () => {
|
|
5
|
-
it( 'serializes basic Error fields', () => {
|
|
6
|
-
const error = new Error( 'boom' );
|
|
7
|
-
|
|
8
|
-
const result = serializeError( error );
|
|
9
|
-
|
|
10
|
-
expect( result ).toMatchObject( {
|
|
11
|
-
name: 'Error',
|
|
12
|
-
message: 'boom'
|
|
13
|
-
} );
|
|
14
|
-
expect( typeof result.stack ).toBe( 'string' );
|
|
15
|
-
expect( result ).not.toHaveProperty( 'cause' );
|
|
16
|
-
} );
|
|
17
|
-
|
|
18
|
-
it( 'preserves custom error constructor names', () => {
|
|
19
|
-
class CustomError extends Error {}
|
|
20
|
-
const error = new CustomError( 'custom boom' );
|
|
21
|
-
|
|
22
|
-
expect( serializeError( error ) ).toMatchObject( {
|
|
23
|
-
name: 'CustomError',
|
|
24
|
-
message: 'custom boom'
|
|
25
|
-
} );
|
|
26
|
-
} );
|
|
27
|
-
|
|
28
|
-
it( 'recursively serializes Error causes', () => {
|
|
29
|
-
const root = new TypeError( 'root failure' );
|
|
30
|
-
const wrapped = new Error( 'wrapped failure', { cause: root } );
|
|
31
|
-
|
|
32
|
-
expect( serializeError( wrapped ) ).toMatchObject( {
|
|
33
|
-
name: 'Error',
|
|
34
|
-
message: 'wrapped failure',
|
|
35
|
-
cause: {
|
|
36
|
-
name: 'TypeError',
|
|
37
|
-
message: 'root failure'
|
|
38
|
-
}
|
|
39
|
-
} );
|
|
40
|
-
} );
|
|
41
|
-
|
|
42
|
-
it( 'preserves JSON-serializable non-Error causes', () => {
|
|
43
|
-
const cause = {
|
|
44
|
-
code: 'bad_input',
|
|
45
|
-
path: [ 'items', 0, 'title' ],
|
|
46
|
-
message: 'Expected title'
|
|
47
|
-
};
|
|
48
|
-
const error = new Error( 'validation failed', { cause } );
|
|
49
|
-
|
|
50
|
-
expect( serializeError( error ) ).toMatchObject( {
|
|
51
|
-
name: 'Error',
|
|
52
|
-
message: 'validation failed',
|
|
53
|
-
cause
|
|
54
|
-
} );
|
|
55
|
-
} );
|
|
56
|
-
|
|
57
|
-
it( 'falls back to inspect for non-JSON-serializable causes', () => {
|
|
58
|
-
const cause = { name: 'circular' };
|
|
59
|
-
cause.self = cause;
|
|
60
|
-
const error = new Error( 'failed', { cause } );
|
|
61
|
-
|
|
62
|
-
const result = serializeError( error );
|
|
63
|
-
|
|
64
|
-
expect( result.cause ).toContain( 'circular' );
|
|
65
|
-
expect( result.cause ).toContain( 'Circular' );
|
|
66
|
-
} );
|
|
67
|
-
|
|
68
|
-
it( 'falls back to inspect for primitive values JSON cannot serialize', () => {
|
|
69
|
-
expect( serializeError( 1n ) ).toBe( '1n' );
|
|
70
|
-
} );
|
|
71
|
-
|
|
72
|
-
it( 'stops serializing cause chains after the depth limit', () => {
|
|
73
|
-
const makeErrorChain = depth => depth === 0 ?
|
|
74
|
-
new Error( 'leaf' ) :
|
|
75
|
-
new Error( `level ${depth}`, { cause: makeErrorChain( depth - 1 ) } );
|
|
76
|
-
const getCauseAtDepth = ( error, depth ) => depth === 0 ?
|
|
77
|
-
error :
|
|
78
|
-
getCauseAtDepth( error.cause, depth - 1 );
|
|
79
|
-
|
|
80
|
-
expect( getCauseAtDepth( serializeError( makeErrorChain( 11 ) ), 11 ) )
|
|
81
|
-
.toEqual( { name: 'Error', message: 'Cause chain too deep' } );
|
|
82
|
-
} );
|
|
83
|
-
} );
|
package/src/worker/proxy.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
2
|
-
import { createChildLogger } from '#logger';
|
|
3
|
-
|
|
4
|
-
const log = createChildLogger( 'Proxy' );
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Routes all `fetch()` calls (including those inside Temporal activities)
|
|
8
|
-
* through an HTTP/HTTPS proxy when standard proxy env vars are set.
|
|
9
|
-
* No-op when none are set.
|
|
10
|
-
*/
|
|
11
|
-
export const bootstrapFetchProxy = () => {
|
|
12
|
-
const httpProxyUrl = process.env.http_proxy ?? process.env.HTTP_PROXY;
|
|
13
|
-
const httpsProxyUrl = process.env.https_proxy ?? process.env.HTTPS_PROXY;
|
|
14
|
-
|
|
15
|
-
if ( httpProxyUrl?.length > 0 || httpsProxyUrl?.length > 0 ) {
|
|
16
|
-
log.info( 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent', { httpProxyUrl, httpsProxyUrl } );
|
|
17
|
-
/* Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299 */
|
|
18
|
-
setGlobalDispatcher( new EnvHttpProxyAgent( { allowH2: false } ) );
|
|
19
|
-
}
|
|
20
|
-
};
|
package/src/worker/proxy.spec.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const mockSetGlobalDispatcher = vi.fn();
|
|
4
|
-
const MockEnvHttpProxyAgent = vi.fn();
|
|
5
|
-
const mockLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
6
|
-
|
|
7
|
-
vi.mock( 'undici', () => ( {
|
|
8
|
-
EnvHttpProxyAgent: MockEnvHttpProxyAgent,
|
|
9
|
-
setGlobalDispatcher: mockSetGlobalDispatcher
|
|
10
|
-
} ) );
|
|
11
|
-
|
|
12
|
-
vi.mock( '#logger', () => ( {
|
|
13
|
-
createChildLogger: () => mockLogger
|
|
14
|
-
} ) );
|
|
15
|
-
|
|
16
|
-
describe( 'worker/proxy', () => {
|
|
17
|
-
const proxyEnv = [ 'http_proxy', 'HTTP_PROXY', 'https_proxy', 'HTTPS_PROXY' ];
|
|
18
|
-
|
|
19
|
-
beforeEach( () => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
for ( const key of proxyEnv ) {
|
|
22
|
-
delete process.env[key];
|
|
23
|
-
}
|
|
24
|
-
} );
|
|
25
|
-
|
|
26
|
-
afterEach( () => {
|
|
27
|
-
vi.resetModules();
|
|
28
|
-
} );
|
|
29
|
-
|
|
30
|
-
it( 'does nothing when no proxy env vars are set', async () => {
|
|
31
|
-
const { bootstrapFetchProxy } = await import( './proxy.js' );
|
|
32
|
-
bootstrapFetchProxy();
|
|
33
|
-
|
|
34
|
-
expect( MockEnvHttpProxyAgent ).not.toHaveBeenCalled();
|
|
35
|
-
expect( mockSetGlobalDispatcher ).not.toHaveBeenCalled();
|
|
36
|
-
expect( mockLogger.info ).not.toHaveBeenCalled();
|
|
37
|
-
} );
|
|
38
|
-
|
|
39
|
-
it( 'sets global dispatcher when proxy URL is available', async () => {
|
|
40
|
-
process.env.HTTP_PROXY = 'http://proxy:8080/';
|
|
41
|
-
const { bootstrapFetchProxy } = await import( './proxy.js' );
|
|
42
|
-
bootstrapFetchProxy();
|
|
43
|
-
|
|
44
|
-
expect( MockEnvHttpProxyAgent ).toHaveBeenCalledWith( { allowH2: false } );
|
|
45
|
-
expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
|
|
46
|
-
expect( mockLogger.info ).toHaveBeenCalledWith(
|
|
47
|
-
'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent',
|
|
48
|
-
{ httpProxyUrl: 'http://proxy:8080/', httpsProxyUrl: undefined }
|
|
49
|
-
);
|
|
50
|
-
} );
|
|
51
|
-
} );
|