@zhivex-ai/core 1.5.0 → 1.7.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/README.md +39 -0
- package/dist/agent-handoff-contracts.d.ts +12 -0
- package/dist/agent-handoff-contracts.d.ts.map +1 -0
- package/dist/agent-handoff-contracts.js +18 -0
- package/dist/agent-handoff-contracts.js.map +1 -0
- package/dist/agent-handoff.d.ts +2 -11
- package/dist/agent-handoff.d.ts.map +1 -1
- package/dist/agent-handoff.js +1 -18
- package/dist/agent-handoff.js.map +1 -1
- package/dist/agent-trace.d.ts +1 -5
- package/dist/agent-trace.d.ts.map +1 -1
- package/dist/agent-trace.js.map +1 -1
- package/dist/agent.d.ts +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +358 -215
- package/dist/agent.js.map +1 -1
- package/dist/api-stability.d.ts.map +1 -1
- package/dist/api-stability.js +36 -25
- package/dist/api-stability.js.map +1 -1
- package/dist/artifact.d.ts +32 -9
- package/dist/artifact.d.ts.map +1 -1
- package/dist/artifact.js +428 -82
- package/dist/artifact.js.map +1 -1
- package/dist/catalog.d.ts +42 -3
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +269 -7
- package/dist/catalog.js.map +1 -1
- package/dist/contracts.d.ts +8 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2 -0
- package/dist/contracts.js.map +1 -0
- package/dist/generate-text.d.ts.map +1 -1
- package/dist/generate-text.js +6 -3
- package/dist/generate-text.js.map +1 -1
- package/dist/google.d.ts +1 -60
- package/dist/google.d.ts.map +1 -1
- package/dist/google.js +0 -198
- package/dist/google.js.map +1 -1
- package/dist/index.d.ts +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/live-agent.js +1 -1
- package/dist/live-agent.js.map +1 -1
- package/dist/middleware.d.ts +16 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +476 -110
- package/dist/middleware.js.map +1 -1
- package/dist/model-capabilities.d.ts +66 -0
- package/dist/model-capabilities.d.ts.map +1 -0
- package/dist/model-capabilities.js +136 -0
- package/dist/model-capabilities.js.map +1 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +8 -0
- package/dist/node.js.map +1 -0
- package/dist/observability.d.ts +57 -10
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +535 -114
- package/dist/observability.js.map +1 -1
- package/dist/pricing.d.ts +7 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +2 -0
- package/dist/pricing.js.map +1 -0
- package/dist/provider-resources.d.ts +61 -0
- package/dist/provider-resources.d.ts.map +1 -0
- package/dist/provider-resources.js +199 -0
- package/dist/provider-resources.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/runner.js.map +1 -1
- package/dist/runtime-entry.d.ts +15 -0
- package/dist/runtime-entry.d.ts.map +1 -0
- package/dist/runtime-entry.js +14 -0
- package/dist/runtime-entry.js.map +1 -0
- package/dist/runtime.d.ts +9 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +35 -8
- package/dist/runtime.js.map +1 -1
- package/dist/secure-id-node.d.ts +2 -0
- package/dist/secure-id-node.d.ts.map +1 -0
- package/dist/secure-id-node.js +3 -0
- package/dist/secure-id-node.js.map +1 -0
- package/dist/secure-id.d.ts.map +1 -1
- package/dist/secure-id.js +6 -2
- package/dist/secure-id.js.map +1 -1
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +4 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +74 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/ui-entry.d.ts +6 -0
- package/dist/ui-entry.d.ts.map +1 -0
- package/dist/ui-entry.js +5 -0
- package/dist/ui-entry.js.map +1 -0
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/workflow-observability.d.ts +13 -0
- package/dist/workflow-observability.d.ts.map +1 -0
- package/dist/workflow-observability.js +186 -0
- package/dist/workflow-observability.js.map +1 -0
- package/dist/workflow-state-contracts.d.ts +76 -0
- package/dist/workflow-state-contracts.d.ts.map +1 -0
- package/dist/workflow-state-contracts.js +49 -0
- package/dist/workflow-state-contracts.js.map +1 -0
- package/dist/workflow-state-service.d.ts +3 -34
- package/dist/workflow-state-service.d.ts.map +1 -1
- package/dist/workflow-state-service.js +2 -2
- package/dist/workflow-state-service.js.map +1 -1
- package/dist/workflow.d.ts +53 -42
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +258 -125
- package/dist/workflow.js.map +1 -1
- package/dist/workflows-entry.d.ts +9 -0
- package/dist/workflows-entry.d.ts.map +1 -0
- package/dist/workflows-entry.js +5 -0
- package/dist/workflows-entry.js.map +1 -0
- package/package.json +42 -1
- package/secure-id-internal.d.ts +1 -0
package/dist/middleware.js
CHANGED
|
@@ -1,7 +1,91 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants, promises as fs } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
import { canonicalStoreFileStem, ensurePrivateDirectory, writePrivateFile } from "./store-security.js";
|
|
6
|
+
const cacheKeySensitiveField = /(?:^|[-_])(api[-_]?key|authorization|cookie|credential|password|secret|token)(?:$|[-_])/iu;
|
|
7
|
+
const cacheKeyOmittedFields = new Set([
|
|
8
|
+
"abortSignal",
|
|
9
|
+
"execute",
|
|
10
|
+
"inputGuardrails",
|
|
11
|
+
"isEnabled",
|
|
12
|
+
"onError",
|
|
13
|
+
"outputGuardrails",
|
|
14
|
+
"signal"
|
|
15
|
+
]);
|
|
16
|
+
const canonicalCacheInput = (value, seen = new WeakSet()) => {
|
|
17
|
+
if (value === null)
|
|
18
|
+
return "null";
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return '"[undefined]"';
|
|
21
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
if (typeof value === "number") {
|
|
24
|
+
if (Number.isNaN(value))
|
|
25
|
+
return '"[number:NaN]"';
|
|
26
|
+
if (value === Number.POSITIVE_INFINITY)
|
|
27
|
+
return '"[number:Infinity]"';
|
|
28
|
+
if (value === Number.NEGATIVE_INFINITY)
|
|
29
|
+
return '"[number:-Infinity]"';
|
|
30
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "bigint")
|
|
33
|
+
return JSON.stringify(`[bigint:${value.toString()}]`);
|
|
34
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
35
|
+
throw new TypeError("Generate cache keys cannot canonicalize functions or symbols.");
|
|
36
|
+
}
|
|
37
|
+
if (value instanceof Date)
|
|
38
|
+
return JSON.stringify(`[date:${value.toISOString()}]`);
|
|
39
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
40
|
+
const bytes = value instanceof ArrayBuffer
|
|
41
|
+
? new Uint8Array(value)
|
|
42
|
+
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
43
|
+
return JSON.stringify(`[bytes:sha256:${createHash("sha256").update(bytes).digest("hex")}]`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof AbortSignal !== "undefined" && value instanceof AbortSignal) {
|
|
46
|
+
return '"[abort-signal]"';
|
|
47
|
+
}
|
|
48
|
+
if (seen.has(value)) {
|
|
49
|
+
throw new TypeError("Generate cache keys cannot canonicalize cyclic input.");
|
|
50
|
+
}
|
|
51
|
+
seen.add(value);
|
|
52
|
+
try {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return `[${value.map((entry) => canonicalCacheInput(entry, seen)).join(",")}]`;
|
|
55
|
+
}
|
|
56
|
+
return `{${Object.keys(value)
|
|
57
|
+
.filter((key) => !cacheKeyOmittedFields.has(key))
|
|
58
|
+
.sort()
|
|
59
|
+
.map((key) => {
|
|
60
|
+
if (cacheKeySensitiveField.test(key)) {
|
|
61
|
+
throw new TypeError("Generate cache keys cannot include sensitive fields.");
|
|
62
|
+
}
|
|
63
|
+
const entry = value[key];
|
|
64
|
+
const serialized = canonicalCacheInput(entry, seen);
|
|
65
|
+
return `${JSON.stringify(key)}:${serialized}`;
|
|
66
|
+
})
|
|
67
|
+
.join(",")}}`;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
seen.delete(value);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const createDefaultGenerateCacheKey = (input) => `generate:v2:${createHash("sha256").update(canonicalCacheInput(input)).digest("hex")}`;
|
|
74
|
+
const defaultGenerateCacheModelScopes = new WeakMap();
|
|
75
|
+
const getDefaultGenerateCacheModelScope = (model) => {
|
|
76
|
+
const existing = defaultGenerateCacheModelScopes.get(model);
|
|
77
|
+
if (existing) {
|
|
78
|
+
return existing;
|
|
79
|
+
}
|
|
80
|
+
const created = `model-instance:${randomUUID()}`;
|
|
81
|
+
defaultGenerateCacheModelScopes.set(model, created);
|
|
82
|
+
return created;
|
|
83
|
+
};
|
|
4
84
|
const telemetryObserversSymbol = Symbol("zhivex-ai.telemetry-observers");
|
|
85
|
+
const DEFAULT_FILE_GENERATE_CACHE_MAX_KEY_BYTES = 1024 * 1024;
|
|
86
|
+
const DEFAULT_FILE_GENERATE_CACHE_MAX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
87
|
+
const FILE_GENERATE_CACHE_READ_CHUNK_BYTES = 64 * 1024;
|
|
88
|
+
const FILE_GENERATE_CACHE_SCHEMA_VERSION = 1;
|
|
5
89
|
const getTelemetryObservers = (model) => [...(model[telemetryObserversSymbol] ?? [])];
|
|
6
90
|
export const emitLanguageModelTelemetryEvent = async (model, event) => {
|
|
7
91
|
const observers = getTelemetryObservers(model);
|
|
@@ -75,13 +159,26 @@ export const wrapLanguageModel = (model, middlewares) => {
|
|
|
75
159
|
return wrappedModel;
|
|
76
160
|
};
|
|
77
161
|
export const createTelemetryMiddleware = (options) => {
|
|
162
|
+
let nextGenerateId = 1;
|
|
163
|
+
let nextStreamId = 1;
|
|
164
|
+
const reportTelemetry = async (event) => {
|
|
165
|
+
try {
|
|
166
|
+
await options.onEvent(event);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Telemetry is best-effort and never owns the model operation outcome.
|
|
170
|
+
}
|
|
171
|
+
};
|
|
78
172
|
const middleware = {
|
|
79
173
|
name: "telemetry",
|
|
80
|
-
onTelemetryEvent:
|
|
174
|
+
onTelemetryEvent: reportTelemetry,
|
|
81
175
|
async wrapGenerate(context, next) {
|
|
176
|
+
const generateId = nextGenerateId;
|
|
177
|
+
nextGenerateId += 1;
|
|
82
178
|
const startedAt = Date.now();
|
|
83
|
-
await
|
|
179
|
+
await reportTelemetry({
|
|
84
180
|
type: "generate-start",
|
|
181
|
+
generateId,
|
|
85
182
|
model: context.model,
|
|
86
183
|
input: context.input,
|
|
87
184
|
startedAt
|
|
@@ -89,8 +186,9 @@ export const createTelemetryMiddleware = (options) => {
|
|
|
89
186
|
try {
|
|
90
187
|
const output = await next();
|
|
91
188
|
const finishedAt = Date.now();
|
|
92
|
-
await
|
|
189
|
+
await reportTelemetry({
|
|
93
190
|
type: "generate-finish",
|
|
191
|
+
generateId,
|
|
94
192
|
model: context.model,
|
|
95
193
|
input: context.input,
|
|
96
194
|
output,
|
|
@@ -103,8 +201,9 @@ export const createTelemetryMiddleware = (options) => {
|
|
|
103
201
|
catch (error) {
|
|
104
202
|
const finishedAt = Date.now();
|
|
105
203
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
106
|
-
await
|
|
204
|
+
await reportTelemetry({
|
|
107
205
|
type: "generate-error",
|
|
206
|
+
generateId,
|
|
108
207
|
model: context.model,
|
|
109
208
|
input: context.input,
|
|
110
209
|
error: err,
|
|
@@ -116,93 +215,189 @@ export const createTelemetryMiddleware = (options) => {
|
|
|
116
215
|
}
|
|
117
216
|
},
|
|
118
217
|
async wrapStream(context, next) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
218
|
+
const streamId = nextStreamId;
|
|
219
|
+
nextStreamId += 1;
|
|
220
|
+
return (async function* () {
|
|
221
|
+
const operationStartedAt = Date.now();
|
|
222
|
+
let finishReason;
|
|
223
|
+
let providerFinishReason;
|
|
224
|
+
let usage;
|
|
225
|
+
let firstChunkAt;
|
|
226
|
+
let previousChunkAt;
|
|
227
|
+
let outputChunkCount = 0;
|
|
228
|
+
let terminalEventEmitted = false;
|
|
229
|
+
try {
|
|
230
|
+
await reportTelemetry({
|
|
231
|
+
type: "stream-start",
|
|
232
|
+
streamId,
|
|
233
|
+
model: context.model,
|
|
234
|
+
input: context.input,
|
|
235
|
+
startedAt: operationStartedAt
|
|
236
|
+
});
|
|
237
|
+
const stream = await next();
|
|
238
|
+
for await (const event of stream) {
|
|
239
|
+
if (event.type === "finish") {
|
|
240
|
+
finishReason = event.finishReason;
|
|
241
|
+
providerFinishReason = event.providerFinishReason;
|
|
242
|
+
usage = event.usage;
|
|
243
|
+
const finishedAt = Date.now();
|
|
244
|
+
terminalEventEmitted = true;
|
|
245
|
+
await reportTelemetry({
|
|
246
|
+
type: "stream-finish",
|
|
247
|
+
streamId,
|
|
248
|
+
model: context.model,
|
|
249
|
+
input: context.input,
|
|
250
|
+
startedAt: operationStartedAt,
|
|
251
|
+
finishedAt,
|
|
252
|
+
latencyMs: finishedAt - operationStartedAt,
|
|
253
|
+
finishReason,
|
|
254
|
+
providerFinishReason,
|
|
255
|
+
usage,
|
|
256
|
+
outputChunkCount
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else if (event.type === "error") {
|
|
260
|
+
const finishedAt = Date.now();
|
|
261
|
+
terminalEventEmitted = true;
|
|
262
|
+
await reportTelemetry({
|
|
263
|
+
type: "stream-error",
|
|
264
|
+
streamId,
|
|
265
|
+
model: context.model,
|
|
266
|
+
input: context.input,
|
|
267
|
+
error: event.error,
|
|
268
|
+
startedAt: operationStartedAt,
|
|
269
|
+
finishedAt,
|
|
270
|
+
latencyMs: finishedAt - operationStartedAt,
|
|
271
|
+
outputChunkCount
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const chunkAt = Date.now();
|
|
276
|
+
firstChunkAt ??= chunkAt;
|
|
277
|
+
const timeSincePreviousChunkMs = previousChunkAt === undefined
|
|
278
|
+
? undefined
|
|
279
|
+
: chunkAt - previousChunkAt;
|
|
280
|
+
previousChunkAt = chunkAt;
|
|
281
|
+
outputChunkCount += 1;
|
|
282
|
+
await reportTelemetry({
|
|
283
|
+
type: "stream-chunk",
|
|
284
|
+
streamId,
|
|
285
|
+
model: context.model,
|
|
286
|
+
input: context.input,
|
|
287
|
+
startedAt: operationStartedAt,
|
|
288
|
+
chunkAt,
|
|
289
|
+
chunkIndex: outputChunkCount,
|
|
290
|
+
timeToFirstChunkMs: outputChunkCount === 1 ? chunkAt - operationStartedAt : undefined,
|
|
291
|
+
timeSincePreviousChunkMs
|
|
292
|
+
});
|
|
140
293
|
}
|
|
294
|
+
yield event;
|
|
295
|
+
if (terminalEventEmitted)
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!terminalEventEmitted) {
|
|
141
299
|
const finishedAt = Date.now();
|
|
142
|
-
|
|
300
|
+
terminalEventEmitted = true;
|
|
301
|
+
await reportTelemetry({
|
|
143
302
|
type: "stream-finish",
|
|
303
|
+
streamId,
|
|
144
304
|
model: context.model,
|
|
145
305
|
input: context.input,
|
|
146
|
-
startedAt,
|
|
306
|
+
startedAt: operationStartedAt,
|
|
147
307
|
finishedAt,
|
|
148
|
-
latencyMs: finishedAt -
|
|
308
|
+
latencyMs: finishedAt - operationStartedAt,
|
|
149
309
|
finishReason,
|
|
150
310
|
providerFinishReason,
|
|
151
|
-
usage
|
|
311
|
+
usage,
|
|
312
|
+
outputChunkCount
|
|
152
313
|
});
|
|
153
314
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
const finishedAt = Date.now();
|
|
318
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
319
|
+
if (!terminalEventEmitted) {
|
|
320
|
+
terminalEventEmitted = true;
|
|
321
|
+
await reportTelemetry({
|
|
158
322
|
type: "stream-error",
|
|
323
|
+
streamId,
|
|
159
324
|
model: context.model,
|
|
160
325
|
input: context.input,
|
|
161
326
|
error: err,
|
|
162
|
-
startedAt,
|
|
327
|
+
startedAt: operationStartedAt,
|
|
163
328
|
finishedAt,
|
|
164
|
-
latencyMs: finishedAt -
|
|
329
|
+
latencyMs: finishedAt - operationStartedAt,
|
|
330
|
+
outputChunkCount
|
|
165
331
|
});
|
|
166
|
-
throw error;
|
|
167
332
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
if (!terminalEventEmitted) {
|
|
337
|
+
terminalEventEmitted = true;
|
|
338
|
+
const finishedAt = Date.now();
|
|
339
|
+
const error = Object.assign(new Error("Stream consumption ended before the provider stream completed."), { name: "AbortError" });
|
|
340
|
+
await reportTelemetry({
|
|
341
|
+
type: "stream-error",
|
|
342
|
+
streamId,
|
|
343
|
+
model: context.model,
|
|
344
|
+
input: context.input,
|
|
345
|
+
error,
|
|
346
|
+
startedAt: operationStartedAt,
|
|
347
|
+
finishedAt,
|
|
348
|
+
latencyMs: finishedAt - operationStartedAt,
|
|
349
|
+
outputChunkCount
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
184
354
|
}
|
|
185
355
|
};
|
|
186
356
|
return middleware;
|
|
187
357
|
};
|
|
188
|
-
export const createCachedGenerateMiddleware = (options) =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const key = options.getKey?.(context.input, context.model) ??
|
|
192
|
-
serializeInput({
|
|
193
|
-
provider: context.model.provider,
|
|
194
|
-
modelId: context.model.modelId,
|
|
195
|
-
input: context.input
|
|
196
|
-
});
|
|
197
|
-
const cached = await options.cache.get(key);
|
|
198
|
-
if (cached) {
|
|
199
|
-
return cached;
|
|
200
|
-
}
|
|
201
|
-
const output = await next();
|
|
202
|
-
await options.cache.set(key, output);
|
|
203
|
-
return output;
|
|
358
|
+
export const createCachedGenerateMiddleware = (options) => {
|
|
359
|
+
if (typeof options.scope === "string" && options.scope.trim().length === 0) {
|
|
360
|
+
throw new ValidationError('The generate cache "scope" must be a non-empty string.');
|
|
204
361
|
}
|
|
205
|
-
|
|
362
|
+
return {
|
|
363
|
+
name: "cache",
|
|
364
|
+
async wrapGenerate(context, next) {
|
|
365
|
+
if (!options.getKey && options.scope === undefined && options.cache.scopeRequirement === "stable") {
|
|
366
|
+
return next();
|
|
367
|
+
}
|
|
368
|
+
let key;
|
|
369
|
+
if (options.getKey) {
|
|
370
|
+
key = options.getKey(context.input, context.model);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const scope = typeof options.scope === "function"
|
|
374
|
+
? options.scope(context.input, context.model)
|
|
375
|
+
: options.scope ?? getDefaultGenerateCacheModelScope(context.model);
|
|
376
|
+
if (typeof scope !== "string" || scope.trim().length === 0) {
|
|
377
|
+
throw new ValidationError('The generate cache "scope" must resolve to a non-empty string.');
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
key = createDefaultGenerateCacheKey({
|
|
381
|
+
scope,
|
|
382
|
+
provider: context.model.provider,
|
|
383
|
+
modelId: context.model.modelId,
|
|
384
|
+
input: context.input
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return next();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const cached = await options.cache.get(key);
|
|
392
|
+
if (cached) {
|
|
393
|
+
return cached;
|
|
394
|
+
}
|
|
395
|
+
const output = await next();
|
|
396
|
+
await options.cache.set(key, output);
|
|
397
|
+
return output;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
};
|
|
206
401
|
export const createInMemoryGenerateCache = () => {
|
|
207
402
|
const store = new Map();
|
|
208
403
|
return {
|
|
@@ -214,13 +409,85 @@ export const createInMemoryGenerateCache = () => {
|
|
|
214
409
|
}
|
|
215
410
|
};
|
|
216
411
|
};
|
|
412
|
+
const positiveFileCacheLimit = (value, fallback, name) => {
|
|
413
|
+
const resolved = value ?? fallback;
|
|
414
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
415
|
+
throw new ValidationError(`The file generate cache "${name}" limit must be a positive safe integer.`);
|
|
416
|
+
}
|
|
417
|
+
return resolved;
|
|
418
|
+
};
|
|
419
|
+
const readFileWithinLimit = async (filePath, maxBytes) => {
|
|
420
|
+
const handle = await fs.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
421
|
+
try {
|
|
422
|
+
const stat = await handle.stat();
|
|
423
|
+
if (!stat.isFile() || stat.size > maxBytes) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
const chunks = [];
|
|
427
|
+
let receivedBytes = 0;
|
|
428
|
+
while (receivedBytes <= maxBytes) {
|
|
429
|
+
const chunk = Buffer.allocUnsafe(Math.min(FILE_GENERATE_CACHE_READ_CHUNK_BYTES, maxBytes - receivedBytes + 1));
|
|
430
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.byteLength, null);
|
|
431
|
+
if (bytesRead === 0) {
|
|
432
|
+
return Buffer.concat(chunks, receivedBytes).toString("utf8");
|
|
433
|
+
}
|
|
434
|
+
receivedBytes += bytesRead;
|
|
435
|
+
if (receivedBytes > maxBytes) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
await handle.close();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
217
446
|
export const createFileGenerateCache = (options) => {
|
|
218
|
-
const
|
|
447
|
+
const maxKeyBytes = positiveFileCacheLimit(options.maxKeyBytes, DEFAULT_FILE_GENERATE_CACHE_MAX_KEY_BYTES, "maxKeyBytes");
|
|
448
|
+
const maxEntryBytes = positiveFileCacheLimit(options.maxEntryBytes, DEFAULT_FILE_GENERATE_CACHE_MAX_ENTRY_BYTES, "maxEntryBytes");
|
|
449
|
+
if (options.ttlMs !== undefined && (!Number.isSafeInteger(options.ttlMs) || options.ttlMs <= 0)) {
|
|
450
|
+
throw new ValidationError('The file generate cache "ttlMs" must be a positive safe integer.');
|
|
451
|
+
}
|
|
452
|
+
const keyIsWithinLimit = (key) => Buffer.byteLength(key, "utf8") <= maxKeyBytes;
|
|
453
|
+
const getPath = (key) => path.join(options.dir, `${canonicalStoreFileStem("generate-cache", [key])}.json`);
|
|
219
454
|
return {
|
|
455
|
+
scopeRequirement: "stable",
|
|
220
456
|
async get(key) {
|
|
457
|
+
if (!keyIsWithinLimit(key)) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
221
460
|
try {
|
|
222
|
-
const file = await
|
|
223
|
-
|
|
461
|
+
const file = await readFileWithinLimit(getPath(key), maxEntryBytes);
|
|
462
|
+
if (file === undefined) {
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const parsed = JSON.parse(file);
|
|
467
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
const envelope = parsed;
|
|
471
|
+
if (envelope.schemaVersion !== FILE_GENERATE_CACHE_SCHEMA_VERSION ||
|
|
472
|
+
typeof envelope.createdAt !== "number" ||
|
|
473
|
+
!Number.isSafeInteger(envelope.createdAt) ||
|
|
474
|
+
envelope.value === null ||
|
|
475
|
+
typeof envelope.value !== "object" ||
|
|
476
|
+
Array.isArray(envelope.value)) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
if (options.ttlMs !== undefined && Date.now() - envelope.createdAt >= options.ttlMs) {
|
|
480
|
+
await fs.unlink(getPath(key)).catch(() => undefined);
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
return envelope.value;
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (error instanceof SyntaxError) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
224
491
|
}
|
|
225
492
|
catch (error) {
|
|
226
493
|
const err = error;
|
|
@@ -231,57 +498,156 @@ export const createFileGenerateCache = (options) => {
|
|
|
231
498
|
}
|
|
232
499
|
},
|
|
233
500
|
async set(key, value) {
|
|
234
|
-
|
|
235
|
-
|
|
501
|
+
if (!keyIsWithinLimit(key)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
let serialized;
|
|
505
|
+
try {
|
|
506
|
+
serialized = JSON.stringify({
|
|
507
|
+
schemaVersion: FILE_GENERATE_CACHE_SCHEMA_VERSION,
|
|
508
|
+
createdAt: Date.now(),
|
|
509
|
+
value
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
if (error instanceof TypeError) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
if (Buffer.byteLength(serialized, "utf8") > maxEntryBytes) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
await ensurePrivateDirectory(options.dir);
|
|
522
|
+
await fs.chmod(options.dir, 0o700);
|
|
523
|
+
await writePrivateFile(getPath(key), serialized);
|
|
236
524
|
}
|
|
237
525
|
};
|
|
238
526
|
};
|
|
239
527
|
export const createCircuitBreakerMiddleware = (options) => {
|
|
240
|
-
const failureThreshold =
|
|
241
|
-
const cooldownMs =
|
|
242
|
-
|
|
528
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
529
|
+
const cooldownMs = options.cooldownMs ?? 30_000;
|
|
530
|
+
if (!Number.isSafeInteger(failureThreshold) || failureThreshold <= 0) {
|
|
531
|
+
throw new ValidationError('The circuit breaker "failureThreshold" must be a positive safe integer.');
|
|
532
|
+
}
|
|
533
|
+
if (!Number.isSafeInteger(cooldownMs) || cooldownMs < 0) {
|
|
534
|
+
throw new ValidationError('The circuit breaker "cooldownMs" must be a non-negative safe integer.');
|
|
535
|
+
}
|
|
536
|
+
const states = new WeakMap();
|
|
537
|
+
const getState = (model) => {
|
|
538
|
+
const existing = states.get(model);
|
|
539
|
+
if (existing) {
|
|
540
|
+
return existing;
|
|
541
|
+
}
|
|
542
|
+
const created = { failures: 0, halfOpenProbeInFlight: false };
|
|
543
|
+
states.set(model, created);
|
|
544
|
+
return created;
|
|
545
|
+
};
|
|
546
|
+
const notifyStateChange = async (model, state, status) => {
|
|
547
|
+
try {
|
|
548
|
+
await options.onStateChange?.({
|
|
549
|
+
failures: state.failures,
|
|
550
|
+
openedAt: state.openedAt,
|
|
551
|
+
model,
|
|
552
|
+
status
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// State observers are operational hooks and must not alter model outcomes.
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const openError = (model) => new Error(`Circuit breaker open for model "${model.provider}/${model.modelId}".`);
|
|
560
|
+
const beginRequest = async (model) => {
|
|
561
|
+
const state = getState(model);
|
|
562
|
+
if (state.openedAt === undefined) {
|
|
563
|
+
return { state, halfOpenProbe: false };
|
|
564
|
+
}
|
|
565
|
+
const cooldownElapsed = Date.now() - state.openedAt >= cooldownMs;
|
|
566
|
+
if (!cooldownElapsed || state.halfOpenProbeInFlight) {
|
|
567
|
+
throw openError(model);
|
|
568
|
+
}
|
|
569
|
+
state.halfOpenProbeInFlight = true;
|
|
570
|
+
await notifyStateChange(model, state, "half-open");
|
|
571
|
+
return { state, halfOpenProbe: true };
|
|
572
|
+
};
|
|
573
|
+
const markSuccess = async (model, permit) => {
|
|
574
|
+
const { state } = permit;
|
|
575
|
+
if (!permit.halfOpenProbe && state.openedAt !== undefined) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const changed = state.failures > 0 || state.openedAt !== undefined || state.halfOpenProbeInFlight;
|
|
579
|
+
state.failures = 0;
|
|
580
|
+
state.openedAt = undefined;
|
|
581
|
+
state.halfOpenProbeInFlight = false;
|
|
582
|
+
if (changed) {
|
|
583
|
+
await notifyStateChange(model, state, "closed");
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
const markFailure = async (model, permit, error) => {
|
|
587
|
+
const { state } = permit;
|
|
588
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
589
|
+
let isFailure = true;
|
|
590
|
+
try {
|
|
591
|
+
isFailure = options.isFailure?.(err) ?? true;
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
isFailure = true;
|
|
595
|
+
}
|
|
596
|
+
if (!isFailure) {
|
|
597
|
+
if (permit.halfOpenProbe) {
|
|
598
|
+
await markSuccess(model, permit);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const wasOpen = state.openedAt !== undefined;
|
|
603
|
+
state.failures += 1;
|
|
604
|
+
if (permit.halfOpenProbe) {
|
|
605
|
+
state.halfOpenProbeInFlight = false;
|
|
606
|
+
state.openedAt = Date.now();
|
|
607
|
+
await notifyStateChange(model, state, "open");
|
|
608
|
+
}
|
|
609
|
+
else if (!wasOpen && state.failures >= failureThreshold) {
|
|
610
|
+
state.openedAt = Date.now();
|
|
611
|
+
await notifyStateChange(model, state, "open");
|
|
612
|
+
}
|
|
613
|
+
};
|
|
243
614
|
return {
|
|
244
615
|
name: "circuit-breaker",
|
|
245
616
|
async wrapGenerate(context, next) {
|
|
246
|
-
const
|
|
247
|
-
if (state.openedAt && now - state.openedAt < cooldownMs) {
|
|
248
|
-
throw new Error(`Circuit breaker open for model "${context.model.provider}/${context.model.modelId}".`);
|
|
249
|
-
}
|
|
250
|
-
if (state.openedAt && now - state.openedAt >= cooldownMs) {
|
|
251
|
-
await options.onStateChange?.({
|
|
252
|
-
...state,
|
|
253
|
-
model: context.model,
|
|
254
|
-
status: "half-open"
|
|
255
|
-
});
|
|
256
|
-
}
|
|
617
|
+
const permit = await beginRequest(context.model);
|
|
257
618
|
try {
|
|
258
619
|
const result = await next();
|
|
259
|
-
|
|
260
|
-
state.openedAt = undefined;
|
|
261
|
-
await options.onStateChange?.({
|
|
262
|
-
...state,
|
|
263
|
-
model: context.model,
|
|
264
|
-
status: "closed"
|
|
265
|
-
});
|
|
620
|
+
await markSuccess(context.model, permit);
|
|
266
621
|
return result;
|
|
267
622
|
}
|
|
268
623
|
catch (error) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
624
|
+
await markFailure(context.model, permit, error);
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
async wrapStream(context, next) {
|
|
629
|
+
return (async function* () {
|
|
630
|
+
const permit = await beginRequest(context.model);
|
|
631
|
+
let settled = false;
|
|
632
|
+
try {
|
|
633
|
+
const stream = await next();
|
|
634
|
+
for await (const event of stream) {
|
|
635
|
+
yield event;
|
|
636
|
+
}
|
|
637
|
+
await markSuccess(context.model, permit);
|
|
638
|
+
settled = true;
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
await markFailure(context.model, permit, error);
|
|
642
|
+
settled = true;
|
|
272
643
|
throw error;
|
|
273
644
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
...state,
|
|
279
|
-
model: context.model,
|
|
280
|
-
status: "open"
|
|
281
|
-
});
|
|
645
|
+
finally {
|
|
646
|
+
if (!settled && permit.halfOpenProbe) {
|
|
647
|
+
permit.state.halfOpenProbeInFlight = false;
|
|
648
|
+
}
|
|
282
649
|
}
|
|
283
|
-
|
|
284
|
-
}
|
|
650
|
+
})();
|
|
285
651
|
}
|
|
286
652
|
};
|
|
287
653
|
};
|