@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.
- 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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { extractSources } from './sources.js';
|
|
2
|
+
import { serializeImagesFromResponse } from './image.js';
|
|
3
|
+
import { addEventActionWithContext, EventAction } from '#tracing';
|
|
4
|
+
import { mapAiError } from './error_handler.js';
|
|
5
|
+
import { isPromise } from 'node:util/types';
|
|
6
|
+
import { FatalError } from '#errors';
|
|
7
|
+
import { Logger } from '#runtime_logger';
|
|
8
|
+
import { Metering } from './metering.js';
|
|
9
|
+
import { randomBytes } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
/** Creates a proxy of the AI SDK response and attach virtual getters to it based on an object map */
|
|
12
|
+
const createResponseProxy = ( { response, properties } ) => new Proxy( response, {
|
|
13
|
+
get( target, prop, receiver ) {
|
|
14
|
+
return Object.hasOwn( properties, prop ) ? properties[prop] : Reflect.get( target, prop, receiver );
|
|
15
|
+
}
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
/** Generate a trace id, starts the tracing and return the id */
|
|
19
|
+
const startTrace = ( { name, prompt } ) => {
|
|
20
|
+
const traceId = `${name}-${Date.now()}-${randomBytes( 4 ).toString( 'hex' )}`;
|
|
21
|
+
addEventActionWithContext( EventAction.START, { kind: 'llm', id: traceId, name, details: { prompt } } );
|
|
22
|
+
return traceId;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Handle AI SDK errors: map them, add an error trace entry and return the new error */
|
|
26
|
+
const handleAiSdkError = ( { traceId, error: originalError } ) => {
|
|
27
|
+
const error = mapAiError( originalError );
|
|
28
|
+
addEventActionWithContext( EventAction.ERROR, { id: traceId, details: error } );
|
|
29
|
+
return error;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handle a failure of our own response handling: wrap it, add an error trace entry and return it.
|
|
34
|
+
* Fatal on purpose - such a failure is deterministic, so a retry pays for the model call again and
|
|
35
|
+
* fails the same way.
|
|
36
|
+
*/
|
|
37
|
+
const handleResponseError = ( { traceId, error: cause } ) => {
|
|
38
|
+
const error = new FatalError( 'AI SDK response handling failed.', { cause } );
|
|
39
|
+
addEventActionWithContext( EventAction.ERROR, { id: traceId, details: error } );
|
|
40
|
+
return error;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Invokes an async function, wrapped in a try/catch, return { result, error } */
|
|
44
|
+
const inlineTryAsync = async fn => {
|
|
45
|
+
try {
|
|
46
|
+
return { result: await fn(), error: null };
|
|
47
|
+
} catch ( error ) {
|
|
48
|
+
return { result: null, error };
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Runs a completing AI SDK text call (`generateText`, `generateTextWithStreaming`,
|
|
54
|
+
* `Agent.generate`, `Agent.generateWithStreaming`): starts the LLM trace, runs `fn`, ends the trace
|
|
55
|
+
* and returns the response proxied with `result` (`text`), `cost` and merged `sources`.
|
|
56
|
+
*
|
|
57
|
+
* `fn` receives `onStepEndHook` to wire into the SDK `onStepEnd`, so the usage of every completed
|
|
58
|
+
* step is collected as the call progresses and still lands when `fn` throws after the model ran.
|
|
59
|
+
* The response `fn` returns is billed directly, and its aggregate usage takes precedence over the
|
|
60
|
+
* collected steps. Trace output keeps raw `response.usage`; normalized usage and cost are trace
|
|
61
|
+
* attributes.
|
|
62
|
+
*
|
|
63
|
+
* Reading the billed response is guarded: a throw from `fn` is mapped as an SDK error, while a
|
|
64
|
+
* failure of our own handling becomes a `FatalError` recorded on the trace. The proxy is built
|
|
65
|
+
* before the trace ends, so a call that fails there never leaves an `end` entry next to an `error`.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} args
|
|
68
|
+
* @param {string} args.name - Trace event name
|
|
69
|
+
* @param {object} args.prompt - Loaded prompt (`config.provider` / `config.model` used for cost)
|
|
70
|
+
* @param {( wiring: { onStepEndHook: Function } ) => Promise<object>} args.fn - AI SDK call; wire
|
|
71
|
+
* `onStepEndHook` into the call `onStepEnd` and return the SDK response
|
|
72
|
+
* @returns {Promise<object>} Proxied SDK response
|
|
73
|
+
*/
|
|
74
|
+
export const wrapTextGeneration = async ( { name, prompt, fn } ) => {
|
|
75
|
+
const traceId = startTrace( { name, prompt } );
|
|
76
|
+
|
|
77
|
+
const metering = new Metering( { traceId, prompt } );
|
|
78
|
+
|
|
79
|
+
const onStepEndHook = metering.recordStep;
|
|
80
|
+
|
|
81
|
+
const { result: response, error } = await inlineTryAsync( () => fn( { onStepEndHook } ) );
|
|
82
|
+
|
|
83
|
+
await metering.bill( response );
|
|
84
|
+
|
|
85
|
+
if ( error ) {
|
|
86
|
+
throw handleAiSdkError( { traceId, error } );
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const { text: result, finalStep, usage } = response;
|
|
91
|
+
const providerMetadata = finalStep?.providerMetadata;
|
|
92
|
+
const sources = extractSources( response );
|
|
93
|
+
|
|
94
|
+
// Create proxy first, as this could theoretically fail
|
|
95
|
+
const responseProxy = createResponseProxy( { response, properties: { cost: metering.attributes.cost, sources, result } } );
|
|
96
|
+
addEventActionWithContext( EventAction.END, { id: traceId, details: { result, usage, providerMetadata, sources } } );
|
|
97
|
+
|
|
98
|
+
return responseProxy;
|
|
99
|
+
} catch ( error ) {
|
|
100
|
+
throw handleResponseError( { traceId, error } );
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Runs `generateImage`: starts the LLM trace, meters the returned usage, ends the trace and returns
|
|
106
|
+
* the response proxied with `result` (the first `image`) and `cost`.
|
|
107
|
+
*
|
|
108
|
+
* Image calls expose no step lifecycle, so usage is read from the response. With no
|
|
109
|
+
* step loop and no output parsing, a throw from `fn` happens before any usage exists, so it is
|
|
110
|
+
* mapped and recorded without metering. Reading the billed response is guarded the same way
|
|
111
|
+
* `wrapTextGeneration` guards it: a failure there is a `FatalError` recorded after billing.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} args
|
|
114
|
+
* @param {string} args.name - Trace event name
|
|
115
|
+
* @param {object} args.prompt - Loaded prompt (`config.provider` / `config.model` used for cost)
|
|
116
|
+
* @param {() => Promise<object>} args.fn - AI SDK call; must return the SDK response
|
|
117
|
+
* @returns {Promise<object>} Proxied SDK response
|
|
118
|
+
*/
|
|
119
|
+
export const wrapImageGeneration = async ( { name, prompt, fn } ) => {
|
|
120
|
+
const traceId = startTrace( { name, prompt } );
|
|
121
|
+
|
|
122
|
+
const metering = new Metering( { traceId, prompt } );
|
|
123
|
+
|
|
124
|
+
const { result: response, error } = await inlineTryAsync( fn );
|
|
125
|
+
|
|
126
|
+
if ( error ) {
|
|
127
|
+
throw handleAiSdkError( { traceId, error } );
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await metering.bill( response );
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const { usage, image: result, providerMetadata } = response;
|
|
134
|
+
const serializeImages = serializeImagesFromResponse( response );
|
|
135
|
+
|
|
136
|
+
// Create proxy first, as this could theoretically fail
|
|
137
|
+
const responseProxy = createResponseProxy( { response, properties: { cost: metering.attributes.cost, result } } );
|
|
138
|
+
|
|
139
|
+
addEventActionWithContext( EventAction.END, { id: traceId, details: { result: serializeImages, usage, providerMetadata } } );
|
|
140
|
+
return responseProxy;
|
|
141
|
+
} catch ( error ) {
|
|
142
|
+
throw handleResponseError( { traceId, error } );
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Awaits a consumer callback, logging and swallowing its failures: the hooks are fire and forget */
|
|
147
|
+
const invokeCallback = async ( cb, args, cbName ) => {
|
|
148
|
+
if ( typeof cb === 'function' ) {
|
|
149
|
+
try {
|
|
150
|
+
await cb( ...args );
|
|
151
|
+
} catch ( e ) {
|
|
152
|
+
Logger.error( `Stream ${cbName}() callback failed`, { namespace: 'LLM', error: e?.message || String( e ) } );
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Starts an LLM trace around a live AI SDK stream (`streamText`, `Agent.stream`).
|
|
159
|
+
*
|
|
160
|
+
* `fn` receives `onEndHook(response, callback)` and `onErrorHook(event, callback)` to wire into the
|
|
161
|
+
* SDK `onEnd` / `onError`, plus `onStepEndHook` and `onAbortHook` for `onStepEnd` / `onAbort`. Both
|
|
162
|
+
* terminal hooks bill, end or record the trace, then invoke `callback`; a `callback` that is not a
|
|
163
|
+
* function is skipped, and callback failures are logged, never rethrown.
|
|
164
|
+
*
|
|
165
|
+
* Neither hook throws, because the SDK invokes them through `notify`, which swallows callback
|
|
166
|
+
* failures and discards their return: a failure while reading the response would vanish, so it is
|
|
167
|
+
* logged and recorded as a trace error here instead. An `onError`
|
|
168
|
+
* event carrying no `Error` is normalized to `Streaming failed.` with the original value as `cause`,
|
|
169
|
+
* so the trace always closes and the consumer callback always runs.
|
|
170
|
+
*
|
|
171
|
+
* Metering is fed by `onStepEndHook`, which collects step usage so the tokens survive the exits
|
|
172
|
+
* that report no response: `NoOutputGeneratedError` skips `onEnd`, and an abort fires neither
|
|
173
|
+
* terminal hook. `onAbortHook` bills that case, and the SDK awaits it before enqueueing the abort
|
|
174
|
+
* part, so billing from there always lands before the consumer observes the cancellation.
|
|
175
|
+
*
|
|
176
|
+
* A throw or rejected Promise from `fn` (stream creation / Agent setup) is mapped and recorded on
|
|
177
|
+
* the trace; a non-Promise return (the `streamText` stream) is returned as is, with a rejection of
|
|
178
|
+
* its `output` promise recorded without being swallowed from the consumer. An abort on
|
|
179
|
+
* `abortSignal` records its reason as a trace error, and the listener is removed when the stream
|
|
180
|
+
* ends, reports an error, or fails during setup. An abort the SDK raises on its own timeout leaves
|
|
181
|
+
* `abortSignal` untouched, so `onAbort` records that one instead.
|
|
182
|
+
*
|
|
183
|
+
* @param {object} args
|
|
184
|
+
* @param {string} args.name - Trace event name
|
|
185
|
+
* @param {object} args.prompt - Loaded prompt (`config.provider` / `config.model` used for cost)
|
|
186
|
+
* @param {AbortSignal} [args.abortSignal] - Optional signal used to trace stream cancellation
|
|
187
|
+
* @param {( wiring: { onEndHook: Function, onErrorHook: Function, onStepEndHook: Function,
|
|
188
|
+
* onAbortHook: Function } ) => object | Promise<object>} args.fn - Wire the hooks into the call
|
|
189
|
+
* and return the SDK stream, or a Promise of that stream (`Agent.stream`)
|
|
190
|
+
* @returns {object | Promise<object>} Value returned by `fn`; a rejected Promise is remapped
|
|
191
|
+
*/
|
|
192
|
+
export const wrapStream = ( { name, prompt, abortSignal, fn } ) => {
|
|
193
|
+
const traceId = startTrace( { name, prompt } );
|
|
194
|
+
const metering = new Metering( { traceId, prompt } );
|
|
195
|
+
|
|
196
|
+
const handleAbort = () => {
|
|
197
|
+
const error = abortSignal?.reason instanceof Error ? abortSignal.reason : new Error( 'Streaming aborted.', { cause: abortSignal?.reason } );
|
|
198
|
+
handleAiSdkError( { traceId, error } );
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if ( abortSignal ) {
|
|
202
|
+
if ( abortSignal.aborted ) {
|
|
203
|
+
handleAbort();
|
|
204
|
+
} else {
|
|
205
|
+
abortSignal.addEventListener( 'abort', handleAbort, { once: true } );
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const removeAbortListener = () => abortSignal?.removeEventListener( 'abort', handleAbort );
|
|
209
|
+
|
|
210
|
+
const onStepEndHook = metering.recordStep;
|
|
211
|
+
|
|
212
|
+
const onAbortHook = async () => {
|
|
213
|
+
removeAbortListener();
|
|
214
|
+
|
|
215
|
+
await metering.bill();
|
|
216
|
+
// abortion was caused by timeout and not signal
|
|
217
|
+
if ( !abortSignal?.aborted ) {
|
|
218
|
+
handleAiSdkError( { traceId, error: new Error( 'Streaming timed out.' ) } );
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const onEndHook = async ( response, callback ) => {
|
|
223
|
+
removeAbortListener();
|
|
224
|
+
|
|
225
|
+
await metering.bill( response );
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const { text: result, finalStep, usage } = response;
|
|
229
|
+
const providerMetadata = finalStep?.providerMetadata;
|
|
230
|
+
const sources = extractSources( response );
|
|
231
|
+
|
|
232
|
+
const proxyResponse = createResponseProxy( { response, properties: { cost: metering.attributes.cost, sources, result } } );
|
|
233
|
+
addEventActionWithContext( EventAction.END, { id: traceId, details: { result, usage, providerMetadata, sources } } );
|
|
234
|
+
|
|
235
|
+
await invokeCallback( callback, [ proxyResponse ], 'onEnd' );
|
|
236
|
+
} catch ( error ) {
|
|
237
|
+
Logger.error( 'AI SDK response handling failed', { namespace: 'LLM', error: error?.message || String( error ) } );
|
|
238
|
+
handleResponseError( { traceId, error } );
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const onErrorHook = async ( event, callback ) => {
|
|
243
|
+
removeAbortListener();
|
|
244
|
+
await metering.bill();
|
|
245
|
+
|
|
246
|
+
const error = event?.error instanceof Error ? event.error : new Error( 'Streaming failed.', { cause: event?.error } );
|
|
247
|
+
const mappedError = handleAiSdkError( { traceId, error } );
|
|
248
|
+
await invokeCallback( callback, [ mappedError ], 'onError' );
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handleStreamError = error => {
|
|
252
|
+
removeAbortListener();
|
|
253
|
+
return handleAiSdkError( { traceId, error } );
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const addStreamOutputErrorHandler = stream => {
|
|
257
|
+
stream.output?.catch( error => handleAiSdkError( { traceId, error } ) );
|
|
258
|
+
return stream;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const createStream = () => {
|
|
262
|
+
try {
|
|
263
|
+
return fn( { onEndHook, onErrorHook, onStepEndHook, onAbortHook } );
|
|
264
|
+
} catch ( error ) {
|
|
265
|
+
throw handleStreamError( error );
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const stream = createStream();
|
|
270
|
+
|
|
271
|
+
/** Streams can be a promise or not, for both cases add the handler to catch .output errors, and if it is a promise add a .catch handler to observe errors there as well */
|
|
272
|
+
return isPromise( stream ) ?
|
|
273
|
+
stream.then( addStreamOutputErrorHandler ).catch( e => {
|
|
274
|
+
throw handleStreamError( e );
|
|
275
|
+
} ) :
|
|
276
|
+
addStreamOutputErrorHandler( stream );
|
|
277
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ValidationError } from '#errors';
|
|
4
|
+
import { Role } from './consts.js';
|
|
5
|
+
import { promptSchema } from './prompt/validations.js';
|
|
6
|
+
|
|
7
|
+
const rejectPromptOwnedCallArg = name => z.unknown().optional().refine(
|
|
8
|
+
value => value === undefined,
|
|
9
|
+
`${name} must be set in the prompt file, not as a call argument`
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// Listed so .strict() callers get the prompt-file message instead of an unrecognized-key error.
|
|
13
|
+
// @TODO Do not drop this later.
|
|
14
|
+
const promptOwnedCallArgRejection = {
|
|
15
|
+
maxSteps: rejectPromptOwnedCallArg( 'maxSteps' ),
|
|
16
|
+
skills: rejectPromptOwnedCallArg( 'skills' )
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const functionSchema = z.custom( value => typeof value === 'function', 'Expected function' );
|
|
20
|
+
|
|
21
|
+
const variablesSchema = z.record( z.string(), z.unknown() );
|
|
22
|
+
|
|
23
|
+
const toolChoiceSchema = z.union( [
|
|
24
|
+
z.enum( [ 'auto', 'none', 'required' ] ),
|
|
25
|
+
z.object( {
|
|
26
|
+
type: z.literal( 'tool' ),
|
|
27
|
+
toolName: z.string().min( 1 )
|
|
28
|
+
} ).strict()
|
|
29
|
+
] );
|
|
30
|
+
|
|
31
|
+
const stopWhenSchema = z.union( [
|
|
32
|
+
functionSchema,
|
|
33
|
+
z.array( functionSchema ).min( 1 )
|
|
34
|
+
] );
|
|
35
|
+
|
|
36
|
+
const isObject = value => value !== null && typeof value === 'object' && !Array.isArray( value );
|
|
37
|
+
|
|
38
|
+
const opaqueObjectSchema = z.custom( isObject, 'Expected object' );
|
|
39
|
+
|
|
40
|
+
const outputSchema = opaqueObjectSchema;
|
|
41
|
+
|
|
42
|
+
const toolsSchema = z.record( z.string().min( 1 ), opaqueObjectSchema );
|
|
43
|
+
|
|
44
|
+
const messageStoreSchema = z.custom(
|
|
45
|
+
value => value !== null &&
|
|
46
|
+
typeof value === 'object' &&
|
|
47
|
+
typeof value.getMessages === 'function' &&
|
|
48
|
+
typeof value.addMessages === 'function',
|
|
49
|
+
'Expected an object with getMessages and addMessages functions'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const modelMessageSchema = z.object( {
|
|
53
|
+
role: z.enum( [ ...Object.values( Role ), 'tool' ] ),
|
|
54
|
+
content: z.union( [ z.string(), z.array( z.unknown() ) ] )
|
|
55
|
+
} ).loose();
|
|
56
|
+
|
|
57
|
+
const promptFileSchema = z.string().min( 1 );
|
|
58
|
+
|
|
59
|
+
const promptInputSchema = z.unknown().transform( ( value, ctx ) => {
|
|
60
|
+
const result = ( isObject( value ) ? promptSchema : promptFileSchema ).safeParse( value );
|
|
61
|
+
if ( !result.success ) {
|
|
62
|
+
for ( const issue of result.error.issues ) {
|
|
63
|
+
ctx.addIssue( issue );
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
return result.data;
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
const promptCallFields = {
|
|
71
|
+
prompt: promptInputSchema,
|
|
72
|
+
promptDir: z.string().min( 1 ).optional(),
|
|
73
|
+
variables: variablesSchema.optional()
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const textRuntimeCallFields = {
|
|
77
|
+
abortSignal: z.instanceof( AbortSignal ).optional(),
|
|
78
|
+
output: outputSchema.optional(),
|
|
79
|
+
stopWhen: stopWhenSchema.optional(),
|
|
80
|
+
toolChoice: toolChoiceSchema.optional(),
|
|
81
|
+
tools: toolsSchema.optional()
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const generateTextCallFields = {
|
|
85
|
+
...promptCallFields,
|
|
86
|
+
...promptOwnedCallArgRejection,
|
|
87
|
+
...textRuntimeCallFields
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const toPromptArgs = ( { prompt, maxSteps, skills, ...rest } ) => ( {
|
|
91
|
+
...( typeof prompt === 'string' ? { promptFile: prompt } : { promptObject: prompt } ),
|
|
92
|
+
...rest
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
const rejectPromptObjectFileArgs = ( args, ctx ) => {
|
|
96
|
+
if ( !isObject( args.prompt ) ) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for ( const field of [ 'promptDir', 'variables' ] ) {
|
|
101
|
+
if ( args[field] !== undefined ) {
|
|
102
|
+
ctx.addIssue( {
|
|
103
|
+
code: 'custom',
|
|
104
|
+
path: [ field ],
|
|
105
|
+
message: `${field} cannot be used when prompt is an object`
|
|
106
|
+
} );
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const generateTextArgsSchema = z.object( generateTextCallFields )
|
|
112
|
+
.strict()
|
|
113
|
+
.superRefine( rejectPromptObjectFileArgs )
|
|
114
|
+
.transform( toPromptArgs );
|
|
115
|
+
|
|
116
|
+
const generateTextWithStreamingArgsSchema = z.object( {
|
|
117
|
+
...generateTextCallFields,
|
|
118
|
+
onChunk: functionSchema.optional()
|
|
119
|
+
} ).strict().superRefine( rejectPromptObjectFileArgs ).transform( toPromptArgs );
|
|
120
|
+
|
|
121
|
+
const streamTextArgsSchema = z.object( {
|
|
122
|
+
...generateTextCallFields,
|
|
123
|
+
onChunk: functionSchema.optional(),
|
|
124
|
+
onError: functionSchema.optional(),
|
|
125
|
+
onEnd: functionSchema.optional()
|
|
126
|
+
} ).strict().superRefine( rejectPromptObjectFileArgs ).transform( toPromptArgs );
|
|
127
|
+
|
|
128
|
+
const agentCallFields = {
|
|
129
|
+
...promptCallFields,
|
|
130
|
+
...promptOwnedCallArgRejection,
|
|
131
|
+
messageStore: messageStoreSchema.optional(),
|
|
132
|
+
output: outputSchema.optional(),
|
|
133
|
+
stopWhen: stopWhenSchema.optional(),
|
|
134
|
+
tools: toolsSchema.optional()
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const agentMethodCallFields = {
|
|
138
|
+
abortSignal: z.instanceof( AbortSignal ).optional(),
|
|
139
|
+
messages: z.array( modelMessageSchema ).default( [] ),
|
|
140
|
+
toolChoice: toolChoiceSchema.optional(),
|
|
141
|
+
...promptOwnedCallArgRejection
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const agentArgsSchema = z.object( agentCallFields )
|
|
145
|
+
.strict()
|
|
146
|
+
.superRefine( rejectPromptObjectFileArgs )
|
|
147
|
+
.transform( toPromptArgs );
|
|
148
|
+
|
|
149
|
+
const agentGenerateArgsSchema = z.object( agentMethodCallFields ).strict();
|
|
150
|
+
|
|
151
|
+
const agentGenerateWithStreamingArgsSchema = z.object( {
|
|
152
|
+
...agentMethodCallFields,
|
|
153
|
+
onChunk: functionSchema.optional()
|
|
154
|
+
} ).strict();
|
|
155
|
+
|
|
156
|
+
const agentStreamArgsSchema = z.object( {
|
|
157
|
+
...agentMethodCallFields,
|
|
158
|
+
onChunk: functionSchema.optional(),
|
|
159
|
+
onError: functionSchema.optional(),
|
|
160
|
+
onEnd: functionSchema.optional()
|
|
161
|
+
} ).strict();
|
|
162
|
+
|
|
163
|
+
const base64StringSchema = z.string()
|
|
164
|
+
.min( 1 )
|
|
165
|
+
.regex(
|
|
166
|
+
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/,
|
|
167
|
+
'Image strings must be raw base64 data.'
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const imageDataSchema = z.union( [
|
|
171
|
+
z.instanceof( Buffer ),
|
|
172
|
+
z.instanceof( Uint8Array ),
|
|
173
|
+
z.instanceof( ArrayBuffer ),
|
|
174
|
+
base64StringSchema
|
|
175
|
+
] );
|
|
176
|
+
|
|
177
|
+
const imageInputSchema = z.union( [
|
|
178
|
+
imageDataSchema,
|
|
179
|
+
z.object( {
|
|
180
|
+
data: imageDataSchema,
|
|
181
|
+
mediaType: z.string().min( 1 ).optional()
|
|
182
|
+
} ).strict()
|
|
183
|
+
] );
|
|
184
|
+
|
|
185
|
+
const generateImageCallFields = {
|
|
186
|
+
abortSignal: z.instanceof( AbortSignal ).optional(),
|
|
187
|
+
images: z.array( imageInputSchema ).min( 1 ).optional(),
|
|
188
|
+
mask: imageInputSchema.optional(),
|
|
189
|
+
...promptCallFields
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const generateImageArgsSchema = z.object( generateImageCallFields ).strict().superRefine( ( args, ctx ) => {
|
|
193
|
+
rejectPromptObjectFileArgs( args, ctx );
|
|
194
|
+
|
|
195
|
+
if ( args.mask && !args.images ) {
|
|
196
|
+
ctx.addIssue( {
|
|
197
|
+
code: 'custom',
|
|
198
|
+
path: [ 'mask' ],
|
|
199
|
+
message: 'mask requires images.'
|
|
200
|
+
} );
|
|
201
|
+
}
|
|
202
|
+
} ).transform( toPromptArgs );
|
|
203
|
+
|
|
204
|
+
const parseSchema = ( schema, input, errorPrefix ) => {
|
|
205
|
+
const result = schema.safeParse( input );
|
|
206
|
+
if ( !result.success ) {
|
|
207
|
+
throw new ValidationError( `${errorPrefix}: ${z.prettifyError( result.error )}` );
|
|
208
|
+
}
|
|
209
|
+
return result.data;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const parseGenerateTextArgs = args =>
|
|
213
|
+
parseSchema( generateTextArgsSchema, args, 'Invalid generateText() arguments' );
|
|
214
|
+
|
|
215
|
+
export const parseGenerateTextWithStreamingArgs = args =>
|
|
216
|
+
parseSchema( generateTextWithStreamingArgsSchema, args, 'Invalid generateTextWithStreaming() arguments' );
|
|
217
|
+
|
|
218
|
+
export const parseStreamTextArgs = args =>
|
|
219
|
+
parseSchema( streamTextArgsSchema, args, 'Invalid streamText() arguments' );
|
|
220
|
+
|
|
221
|
+
export const parseGenerateImageArgs = args =>
|
|
222
|
+
parseSchema( generateImageArgsSchema, args, 'Invalid generateImage() arguments' );
|
|
223
|
+
|
|
224
|
+
export const parseAgentArgs = args =>
|
|
225
|
+
parseSchema( agentArgsSchema, args, 'Invalid Agent() arguments' );
|
|
226
|
+
|
|
227
|
+
export const parseAgentGenerateArgs = args =>
|
|
228
|
+
parseSchema( agentGenerateArgsSchema, args ?? {}, 'Invalid Agent.generate() arguments' );
|
|
229
|
+
|
|
230
|
+
export const parseAgentGenerateWithStreamingArgs = args =>
|
|
231
|
+
parseSchema( agentGenerateWithStreamingArgsSchema, args ?? {}, 'Invalid Agent.generateWithStreaming() arguments' );
|
|
232
|
+
|
|
233
|
+
export const parseAgentStreamArgs = args =>
|
|
234
|
+
parseSchema( agentStreamArgsSchema, args ?? {}, 'Invalid Agent.stream() arguments' );
|
package/src/sdk/helpers/path.js
CHANGED
|
@@ -17,6 +17,7 @@ const defaultIgnorePaths = [
|
|
|
17
17
|
'webpack/bootstrap'
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
+
/** @deprecated Internal helper with no public replacement, kept only for `@outputai/evals`. It will be removed when evals folds into `@outputai/core`. */
|
|
20
21
|
export const Path = {
|
|
21
22
|
resolveInvocationDir: ( additionalIgnorePaths = [] ) => {
|
|
22
23
|
const stack = new Error().stack;
|
|
@@ -4,22 +4,5 @@ export declare namespace Attribute {
|
|
|
4
4
|
constructor( type: string );
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export class HTTPRequestCount extends BaseAttribute {
|
|
8
|
-
static TYPE: 'http:request:count';
|
|
9
|
-
type: typeof HTTPRequestCount.TYPE;
|
|
10
|
-
url: string;
|
|
11
|
-
requestId: string;
|
|
12
|
-
constructor( url: string, requestId: string );
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class HTTPRequestCost extends BaseAttribute {
|
|
16
|
-
static TYPE: 'http:request:cost';
|
|
17
|
-
type: typeof HTTPRequestCost.TYPE;
|
|
18
|
-
url: string;
|
|
19
|
-
requestId: string;
|
|
20
|
-
total: number;
|
|
21
|
-
constructor( url: string, requestId: string, total: number );
|
|
22
|
-
}
|
|
23
|
-
|
|
24
7
|
export type Instance = BaseAttribute;
|
|
25
8
|
}
|
|
@@ -9,37 +9,9 @@ export class BaseAttribute {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
class HTTPRequestCount extends BaseAttribute {
|
|
13
|
-
static TYPE = 'http:request:count';
|
|
14
|
-
url;
|
|
15
|
-
requestId;
|
|
16
|
-
|
|
17
|
-
constructor( url, requestId ) {
|
|
18
|
-
super( HTTPRequestCount.TYPE );
|
|
19
|
-
this.url = url;
|
|
20
|
-
this.requestId = requestId;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
class HTTPRequestCost extends BaseAttribute {
|
|
25
|
-
static TYPE = 'http:request:cost';
|
|
26
|
-
url;
|
|
27
|
-
requestId;
|
|
28
|
-
total = 0;
|
|
29
|
-
|
|
30
|
-
constructor( url, requestId, total ) {
|
|
31
|
-
super( HTTPRequestCost.TYPE );
|
|
32
|
-
this.url = url;
|
|
33
|
-
this.requestId = requestId;
|
|
34
|
-
this.total = total;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
12
|
/**
|
|
39
13
|
* Types of ADD_ATTR attributes
|
|
40
14
|
*/
|
|
41
15
|
export const Attribute = {
|
|
42
|
-
BaseAttribute
|
|
43
|
-
HTTPRequestCost,
|
|
44
|
-
HTTPRequestCount
|
|
16
|
+
BaseAttribute
|
|
45
17
|
};
|
package/src/worker/configs.js
CHANGED
|
@@ -22,13 +22,31 @@ const envVarSchema = z.object( {
|
|
|
22
22
|
// Worker concurrency — tune these via env vars to adjust for your workload.
|
|
23
23
|
// Each step (API, LLM, etc.) call is one activity. Lower this to reduce memory pressure.
|
|
24
24
|
TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 40 ) ),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/*
|
|
26
|
+
Workflow tasks that may run at once. These are short state transitions, so this is rarely the
|
|
27
|
+
throughput limit - activity concurrency above is. Left unset so @temporalio/worker applies its
|
|
28
|
+
own default of 40; the startup 'Worker sizing' log reports it as workflowTaskSlots.
|
|
29
|
+
https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#maxconcurrentworkflowtaskexecutions
|
|
30
|
+
*/
|
|
31
|
+
TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTIONS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().optional() ),
|
|
32
|
+
/*
|
|
33
|
+
LRU cache for sticky workflow execution, holding each workflow's state until it completes or is
|
|
34
|
+
evicted. Left unset so @temporalio/worker sizes it from the isolate's heap limit, which a flat
|
|
35
|
+
value here cannot track. That formula assumes roughly 600 cached workflows per GB, so set this
|
|
36
|
+
where payloads are heavier - the startup 'Worker sizing' log reports what it resolved to.
|
|
37
|
+
https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#maxcachedworkflows
|
|
38
|
+
*/
|
|
39
|
+
TEMPORAL_MAX_CACHED_WORKFLOWS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().optional() ),
|
|
29
40
|
// How aggressively the worker pulls tasks from Temporal.
|
|
30
41
|
TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 5 ) ),
|
|
31
42
|
TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 5 ) ),
|
|
43
|
+
/*
|
|
44
|
+
Threads running workflow sandboxes. Each is a V8 isolate and NODE_OPTIONS heap caps apply per
|
|
45
|
+
isolate, so this multiplies the heap the process can commit - the startup 'Worker sizing'
|
|
46
|
+
log reports the total. Left unset so @temporalio/worker picks: 1 with reuseV8Context, 2 without.
|
|
47
|
+
https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#workflowthreadpoolsize
|
|
48
|
+
*/
|
|
49
|
+
TEMPORAL_WORKFLOW_THREAD_POOL_SIZE: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().optional() ),
|
|
32
50
|
// JSON-encoded Temporal Worker tuner options.
|
|
33
51
|
TEMPORAL_WORKER_TUNER: workerTunerEnvSchema,
|
|
34
52
|
// Activity configs
|
|
@@ -64,6 +82,7 @@ export const maxConcurrentWorkflowTaskExecutions = envVars.TEMPORAL_MAX_CONCURRE
|
|
|
64
82
|
export const maxCachedWorkflows = envVars.TEMPORAL_MAX_CACHED_WORKFLOWS;
|
|
65
83
|
export const maxConcurrentActivityTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS;
|
|
66
84
|
export const maxConcurrentWorkflowTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS;
|
|
85
|
+
export const workflowThreadPoolSize = envVars.TEMPORAL_WORKFLOW_THREAD_POOL_SIZE;
|
|
67
86
|
export const workerTuner = envVars.TEMPORAL_WORKER_TUNER;
|
|
68
87
|
export const namespace = envVars.TEMPORAL_NAMESPACE;
|
|
69
88
|
export const taskQueue = envVars.OUTPUT_CATALOG_ID;
|