@graphorin/provider 0.5.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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/llamacpp-server.d.ts +69 -0
- package/dist/adapters/llamacpp-server.d.ts.map +1 -0
- package/dist/adapters/llamacpp-server.js +51 -0
- package/dist/adapters/llamacpp-server.js.map +1 -0
- package/dist/adapters/ollama.d.ts +43 -0
- package/dist/adapters/ollama.d.ts.map +1 -0
- package/dist/adapters/ollama.js +246 -0
- package/dist/adapters/ollama.js.map +1 -0
- package/dist/adapters/openai-compatible.d.ts +48 -0
- package/dist/adapters/openai-compatible.d.ts.map +1 -0
- package/dist/adapters/openai-compatible.js +28 -0
- package/dist/adapters/openai-compatible.js.map +1 -0
- package/dist/adapters/vercel.d.ts +135 -0
- package/dist/adapters/vercel.d.ts.map +1 -0
- package/dist/adapters/vercel.js +306 -0
- package/dist/adapters/vercel.js.map +1 -0
- package/dist/counters/anthropic.d.ts +36 -0
- package/dist/counters/anthropic.d.ts.map +1 -0
- package/dist/counters/anthropic.js +63 -0
- package/dist/counters/anthropic.js.map +1 -0
- package/dist/counters/bedrock.d.ts +27 -0
- package/dist/counters/bedrock.d.ts.map +1 -0
- package/dist/counters/bedrock.js +29 -0
- package/dist/counters/bedrock.js.map +1 -0
- package/dist/counters/dispatcher.d.ts +34 -0
- package/dist/counters/dispatcher.d.ts.map +1 -0
- package/dist/counters/dispatcher.js +78 -0
- package/dist/counters/dispatcher.js.map +1 -0
- package/dist/counters/global.d.ts +26 -0
- package/dist/counters/global.d.ts.map +1 -0
- package/dist/counters/global.js +31 -0
- package/dist/counters/global.js.map +1 -0
- package/dist/counters/google.d.ts +27 -0
- package/dist/counters/google.d.ts.map +1 -0
- package/dist/counters/google.js +29 -0
- package/dist/counters/google.js.map +1 -0
- package/dist/counters/heuristic.d.ts +37 -0
- package/dist/counters/heuristic.d.ts.map +1 -0
- package/dist/counters/heuristic.js +64 -0
- package/dist/counters/heuristic.js.map +1 -0
- package/dist/counters/index.d.ts +9 -0
- package/dist/counters/index.js +10 -0
- package/dist/counters/js-tiktoken.d.ts +63 -0
- package/dist/counters/js-tiktoken.d.ts.map +1 -0
- package/dist/counters/js-tiktoken.js +73 -0
- package/dist/counters/js-tiktoken.js.map +1 -0
- package/dist/counters/serialize.d.ts +37 -0
- package/dist/counters/serialize.d.ts.map +1 -0
- package/dist/counters/serialize.js +57 -0
- package/dist/counters/serialize.js.map +1 -0
- package/dist/errors/errors.d.ts +167 -0
- package/dist/errors/errors.d.ts.map +1 -0
- package/dist/errors/errors.js +182 -0
- package/dist/errors/errors.js.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/abort.js +34 -0
- package/dist/internal/abort.js.map +1 -0
- package/dist/internal/http.js +163 -0
- package/dist/internal/http.js.map +1 -0
- package/dist/internal/openai-shaped.js +308 -0
- package/dist/internal/openai-shaped.js.map +1 -0
- package/dist/internal/sse.js +104 -0
- package/dist/internal/sse.js.map +1 -0
- package/dist/internal/url-utils.js +24 -0
- package/dist/internal/url-utils.js.map +1 -0
- package/dist/middleware/compose.d.ts +85 -0
- package/dist/middleware/compose.d.ts.map +1 -0
- package/dist/middleware/compose.js +174 -0
- package/dist/middleware/compose.js.map +1 -0
- package/dist/middleware/index.d.ts +10 -0
- package/dist/middleware/index.js +11 -0
- package/dist/middleware/production-hook.d.ts +27 -0
- package/dist/middleware/production-hook.d.ts.map +1 -0
- package/dist/middleware/production-hook.js +21 -0
- package/dist/middleware/production-hook.js.map +1 -0
- package/dist/middleware/with-cost-limit.d.ts +46 -0
- package/dist/middleware/with-cost-limit.d.ts.map +1 -0
- package/dist/middleware/with-cost-limit.js +66 -0
- package/dist/middleware/with-cost-limit.js.map +1 -0
- package/dist/middleware/with-cost-tracking.d.ts +91 -0
- package/dist/middleware/with-cost-tracking.d.ts.map +1 -0
- package/dist/middleware/with-cost-tracking.js +95 -0
- package/dist/middleware/with-cost-tracking.js.map +1 -0
- package/dist/middleware/with-fallback.d.ts +25 -0
- package/dist/middleware/with-fallback.d.ts.map +1 -0
- package/dist/middleware/with-fallback.js +84 -0
- package/dist/middleware/with-fallback.js.map +1 -0
- package/dist/middleware/with-rate-limit.d.ts +36 -0
- package/dist/middleware/with-rate-limit.d.ts.map +1 -0
- package/dist/middleware/with-rate-limit.js +141 -0
- package/dist/middleware/with-rate-limit.js.map +1 -0
- package/dist/middleware/with-redaction.d.ts +60 -0
- package/dist/middleware/with-redaction.d.ts.map +1 -0
- package/dist/middleware/with-redaction.js +305 -0
- package/dist/middleware/with-redaction.js.map +1 -0
- package/dist/middleware/with-retry.d.ts +26 -0
- package/dist/middleware/with-retry.d.ts.map +1 -0
- package/dist/middleware/with-retry.js +157 -0
- package/dist/middleware/with-retry.js.map +1 -0
- package/dist/middleware/with-tracing.d.ts +21 -0
- package/dist/middleware/with-tracing.d.ts.map +1 -0
- package/dist/middleware/with-tracing.js +55 -0
- package/dist/middleware/with-tracing.js.map +1 -0
- package/dist/model-tier/classify.d.ts +34 -0
- package/dist/model-tier/classify.d.ts.map +1 -0
- package/dist/model-tier/classify.js +117 -0
- package/dist/model-tier/classify.js.map +1 -0
- package/dist/model-tier/index.d.ts +2 -0
- package/dist/model-tier/index.js +3 -0
- package/dist/provider.d.ts +51 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +64 -0
- package/dist/provider.js.map +1 -0
- package/dist/reasoning/apply-policy.d.ts +22 -0
- package/dist/reasoning/apply-policy.d.ts.map +1 -0
- package/dist/reasoning/apply-policy.js +47 -0
- package/dist/reasoning/apply-policy.js.map +1 -0
- package/dist/reasoning/classify-contract.d.ts +41 -0
- package/dist/reasoning/classify-contract.d.ts.map +1 -0
- package/dist/reasoning/classify-contract.js +59 -0
- package/dist/reasoning/classify-contract.js.map +1 -0
- package/dist/reasoning/index.d.ts +4 -0
- package/dist/reasoning/index.js +5 -0
- package/dist/reasoning/retention.d.ts +39 -0
- package/dist/reasoning/retention.d.ts.map +1 -0
- package/dist/reasoning/retention.js +33 -0
- package/dist/reasoning/retention.js.map +1 -0
- package/dist/tool-examples.d.ts +14 -0
- package/dist/tool-examples.d.ts.map +1 -0
- package/dist/tool-examples.js +35 -0
- package/dist/tool-examples.js.map +1 -0
- package/dist/trust/classify-local-provider.d.ts +47 -0
- package/dist/trust/classify-local-provider.d.ts.map +1 -0
- package/dist/trust/classify-local-provider.js +164 -0
- package/dist/trust/classify-local-provider.js.map +1 -0
- package/dist/trust/index.d.ts +3 -0
- package/dist/trust/index.js +3 -0
- package/package.json +128 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { PromptRedactionError } from "../errors/errors.js";
|
|
2
|
+
import { defineProviderMiddleware } from "./compose.js";
|
|
3
|
+
import { SECRET_VALUE_BRAND } from "@graphorin/core";
|
|
4
|
+
import { BUILT_IN_PATTERNS } from "@graphorin/observability/redaction/patterns";
|
|
5
|
+
|
|
6
|
+
//#region src/middleware/with-redaction.ts
|
|
7
|
+
const TRUST_CLASS_DEFAULTS = Object.freeze({
|
|
8
|
+
loopback: { scanScope: "secret-value-only" },
|
|
9
|
+
private: {
|
|
10
|
+
scanScope: "all",
|
|
11
|
+
failClosed: false
|
|
12
|
+
},
|
|
13
|
+
"public-tls": {
|
|
14
|
+
scanScope: "all",
|
|
15
|
+
failClosed: false
|
|
16
|
+
},
|
|
17
|
+
"public-cleartext": {}
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* @stable
|
|
21
|
+
*/
|
|
22
|
+
const withRedaction = defineProviderMiddleware({
|
|
23
|
+
kind: "withRedaction",
|
|
24
|
+
factory: (rawPolicy) => {
|
|
25
|
+
const baseLogger = rawPolicy.logger ?? defaultLogger;
|
|
26
|
+
return (next) => {
|
|
27
|
+
const trustClass = rawPolicy.trustClassOverride ?? inferTrustClass(next.acceptsSensitivity);
|
|
28
|
+
const policy = effectivePolicy(rawPolicy, trustClass);
|
|
29
|
+
return {
|
|
30
|
+
name: next.name,
|
|
31
|
+
modelId: next.modelId,
|
|
32
|
+
capabilities: next.capabilities,
|
|
33
|
+
...next.acceptsSensitivity !== void 0 ? { acceptsSensitivity: next.acceptsSensitivity } : {},
|
|
34
|
+
stream(req) {
|
|
35
|
+
const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);
|
|
36
|
+
return scanStreamingResponse(next.stream(scrubbed), policy, baseLogger);
|
|
37
|
+
},
|
|
38
|
+
async generate(req) {
|
|
39
|
+
const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);
|
|
40
|
+
return next.generate(scrubbed);
|
|
41
|
+
},
|
|
42
|
+
...next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
function effectivePolicy(raw, trustClass) {
|
|
48
|
+
const tierDefaults = trustClass !== void 0 ? TRUST_CLASS_DEFAULTS[trustClass] : {};
|
|
49
|
+
const explicitOverride = trustClass !== void 0 ? raw.byTrustClass?.[trustClass] ?? {} : {};
|
|
50
|
+
const merged = {
|
|
51
|
+
patterns: ensureGlobalPatterns(raw.patterns ?? explicitOverride.patterns ?? tierDefaults.patterns ?? BUILT_IN_PATTERNS),
|
|
52
|
+
action: raw.action ?? explicitOverride.action ?? tierDefaults.action ?? "redact",
|
|
53
|
+
failClosed: pick(raw.failClosed, explicitOverride.failClosed, tierDefaults.failClosed, false),
|
|
54
|
+
scanScope: raw.scanScope ?? explicitOverride.scanScope ?? tierDefaults.scanScope ?? "all",
|
|
55
|
+
detectSecretValue: pick(raw.detectSecretValue, explicitOverride.detectSecretValue, tierDefaults.detectSecretValue, true),
|
|
56
|
+
stripCacheControlOnHit: pick(raw.stripCacheControlOnHit, explicitOverride.stripCacheControlOnHit, tierDefaults.stripCacheControlOnHit, true),
|
|
57
|
+
onViolation: raw.onViolation
|
|
58
|
+
};
|
|
59
|
+
if (raw.failClosed === true || explicitOverride.failClosed === true) merged.action = "throw";
|
|
60
|
+
return merged;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* PS-22: callers can supply a pattern without the global flag. `.replace` /
|
|
64
|
+
* `.match` then stop after the FIRST occurrence, so a second secret on the same
|
|
65
|
+
* line is neither redacted nor counted. Re-compile every non-global pattern
|
|
66
|
+
* with the `g` flag so all occurrences are covered. The 14 built-ins already
|
|
67
|
+
* carry `/g`; this only rescues user-supplied patterns (identity otherwise).
|
|
68
|
+
*/
|
|
69
|
+
function ensureGlobalPatterns(patterns) {
|
|
70
|
+
let changed = false;
|
|
71
|
+
const out = patterns.map((p) => {
|
|
72
|
+
if (p.regex.global) return p;
|
|
73
|
+
changed = true;
|
|
74
|
+
return {
|
|
75
|
+
...p,
|
|
76
|
+
regex: new RegExp(p.regex.source, `${p.regex.flags}g`)
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return changed ? out : patterns;
|
|
80
|
+
}
|
|
81
|
+
function pick(...values) {
|
|
82
|
+
for (const v of values) if (v !== void 0) return v;
|
|
83
|
+
return values[values.length - 1];
|
|
84
|
+
}
|
|
85
|
+
function scrubRequest(req, policy, trustClass, logger) {
|
|
86
|
+
const ctx = {
|
|
87
|
+
hits: 0,
|
|
88
|
+
cacheControlHits: 0
|
|
89
|
+
};
|
|
90
|
+
const messages = req.messages.map((msg, i) => scrubMessage(msg, policy, trustClass, `messages[${i}]`, ctx, logger));
|
|
91
|
+
const systemMessage = req.systemMessage !== void 0 ? scrubText(req.systemMessage, "system", "systemMessage", policy, trustClass, ctx, logger) : void 0;
|
|
92
|
+
let providerOptions = scrubProviderOptions(req.providerOptions, policy, trustClass, ctx, logger);
|
|
93
|
+
if (policy.stripCacheControlOnHit && ctx.hits > 0 && providerOptions !== void 0 && hasAnthropicCacheControl(providerOptions)) {
|
|
94
|
+
providerOptions = stripAnthropicCacheControl(providerOptions);
|
|
95
|
+
reportViolation(policy, {
|
|
96
|
+
patternName: "anthropic-cache-control-stripped",
|
|
97
|
+
fieldPath: "providerOptions.anthropic.cache_control",
|
|
98
|
+
matchLength: 0,
|
|
99
|
+
...trustClass !== void 0 ? { trustClass } : {},
|
|
100
|
+
action: "redact"
|
|
101
|
+
}, logger);
|
|
102
|
+
ctx.cacheControlHits++;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...req,
|
|
106
|
+
messages,
|
|
107
|
+
...systemMessage !== void 0 ? { systemMessage } : {},
|
|
108
|
+
...providerOptions !== void 0 ? { providerOptions } : {}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function hasAnthropicCacheControl(providerOptions) {
|
|
112
|
+
const anthropic = providerOptions.anthropic;
|
|
113
|
+
if (anthropic === null || typeof anthropic !== "object") return false;
|
|
114
|
+
return "cache_control" in anthropic;
|
|
115
|
+
}
|
|
116
|
+
function stripAnthropicCacheControl(providerOptions) {
|
|
117
|
+
const anthropic = providerOptions.anthropic;
|
|
118
|
+
if (anthropic === null || typeof anthropic !== "object") return providerOptions;
|
|
119
|
+
const { cache_control: _drop, ...rest } = anthropic;
|
|
120
|
+
return {
|
|
121
|
+
...providerOptions,
|
|
122
|
+
anthropic: rest
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function scrubMessage(msg, policy, trustClass, fieldPrefix, ctx, logger) {
|
|
126
|
+
if (policy.scanScope === "secret-value-only") {
|
|
127
|
+
detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);
|
|
128
|
+
return msg;
|
|
129
|
+
}
|
|
130
|
+
if (policy.scanScope === "untrusted" && msg.role === "system") {
|
|
131
|
+
detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);
|
|
132
|
+
return msg;
|
|
133
|
+
}
|
|
134
|
+
switch (msg.role) {
|
|
135
|
+
case "system": return {
|
|
136
|
+
role: "system",
|
|
137
|
+
content: scrubText(msg.content, "system", `${fieldPrefix}.content`, policy, trustClass, ctx, logger)
|
|
138
|
+
};
|
|
139
|
+
case "user": return scrubUserMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);
|
|
140
|
+
case "assistant": return scrubAssistantMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);
|
|
141
|
+
case "tool": return scrubToolMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);
|
|
142
|
+
default: return msg;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function scrubUserMessage(msg, policy, trustClass, fieldPrefix, ctx, logger) {
|
|
146
|
+
return {
|
|
147
|
+
role: "user",
|
|
148
|
+
content: scrubContent(msg.content, "user", `${fieldPrefix}.content`, policy, trustClass, ctx, logger),
|
|
149
|
+
...msg.userId !== void 0 ? { userId: msg.userId } : {}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function scrubAssistantMessage(msg, policy, trustClass, fieldPrefix, ctx, logger) {
|
|
153
|
+
const next = {
|
|
154
|
+
role: "assistant",
|
|
155
|
+
content: scrubContent(msg.content, "assistant", `${fieldPrefix}.content`, policy, trustClass, ctx, logger),
|
|
156
|
+
...msg.agentId !== void 0 ? { agentId: msg.agentId } : {}
|
|
157
|
+
};
|
|
158
|
+
if (msg.toolCalls !== void 0) next.toolCalls = msg.toolCalls.map((tc, idx) => ({
|
|
159
|
+
...tc,
|
|
160
|
+
args: scrubAny(tc.args, "assistant", `${fieldPrefix}.toolCalls[${idx}].args`, policy, trustClass, ctx, logger)
|
|
161
|
+
}));
|
|
162
|
+
return next;
|
|
163
|
+
}
|
|
164
|
+
function scrubToolMessage(msg, policy, trustClass, fieldPrefix, ctx, logger) {
|
|
165
|
+
return {
|
|
166
|
+
role: "tool",
|
|
167
|
+
toolCallId: msg.toolCallId,
|
|
168
|
+
content: scrubContent(msg.content, "tool", `${fieldPrefix}.content`, policy, trustClass, ctx, logger)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function scrubContent(content, role, fieldPath, policy, trustClass, ctx, logger) {
|
|
172
|
+
if (typeof content === "string") return scrubText(content, role, fieldPath, policy, trustClass, ctx, logger);
|
|
173
|
+
return content.map((part, idx) => {
|
|
174
|
+
if (typeof part === "string") return scrubText(part, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger);
|
|
175
|
+
if (part === null || typeof part !== "object") return part;
|
|
176
|
+
const partWithType = part;
|
|
177
|
+
if (partWithType.type === "text" && typeof partWithType.text === "string") return {
|
|
178
|
+
...part,
|
|
179
|
+
text: scrubText(partWithType.text, role, `${fieldPath}[${idx}].text`, policy, trustClass, ctx, logger)
|
|
180
|
+
};
|
|
181
|
+
if (partWithType.type === "reasoning" && typeof partWithType.text === "string") return {
|
|
182
|
+
...part,
|
|
183
|
+
text: scrubText(partWithType.text, role, `${fieldPath}[${idx}].text`, policy, trustClass, ctx, logger)
|
|
184
|
+
};
|
|
185
|
+
return part;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function scrubText(text, role, fieldPath, policy, trustClass, ctx, logger) {
|
|
189
|
+
let scrubbed = text;
|
|
190
|
+
for (const pattern of policy.patterns) {
|
|
191
|
+
pattern.regex.lastIndex = 0;
|
|
192
|
+
if (pattern.regex.test(scrubbed)) {
|
|
193
|
+
const matchLength = (scrubbed.match(pattern.regex) ?? []).reduce((sum, m) => sum + m.length, 0);
|
|
194
|
+
ctx.hits++;
|
|
195
|
+
reportViolation(policy, {
|
|
196
|
+
patternName: pattern.name,
|
|
197
|
+
fieldPath,
|
|
198
|
+
role,
|
|
199
|
+
matchLength,
|
|
200
|
+
...trustClass !== void 0 ? { trustClass } : {},
|
|
201
|
+
action: policy.action
|
|
202
|
+
}, logger);
|
|
203
|
+
if (policy.action === "throw" || policy.failClosed) throw new PromptRedactionError({
|
|
204
|
+
patternName: pattern.name,
|
|
205
|
+
fieldPath,
|
|
206
|
+
...role !== void 0 ? { role } : {}
|
|
207
|
+
});
|
|
208
|
+
pattern.regex.lastIndex = 0;
|
|
209
|
+
scrubbed = scrubbed.replace(pattern.regex, pattern.mask ?? `[REDACTED ${pattern.name}]`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return scrubbed;
|
|
213
|
+
}
|
|
214
|
+
function scrubAny(value, role, fieldPath, policy, trustClass, ctx, logger) {
|
|
215
|
+
if (typeof value === "string") return scrubText(value, role, fieldPath, policy, trustClass, ctx, logger);
|
|
216
|
+
if (value === null || value === void 0) return value;
|
|
217
|
+
if (Array.isArray(value)) return value.map((item, idx) => scrubAny(item, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger));
|
|
218
|
+
if (typeof value === "object") {
|
|
219
|
+
if (policy.detectSecretValue && isSecretValue(value)) {
|
|
220
|
+
ctx.hits++;
|
|
221
|
+
reportViolation(policy, {
|
|
222
|
+
patternName: "graphorin-secret-value",
|
|
223
|
+
fieldPath,
|
|
224
|
+
role,
|
|
225
|
+
matchLength: 0,
|
|
226
|
+
...trustClass !== void 0 ? { trustClass } : {},
|
|
227
|
+
action: policy.action
|
|
228
|
+
}, logger);
|
|
229
|
+
return "[REDACTED graphorin-secret-value]";
|
|
230
|
+
}
|
|
231
|
+
const out = {};
|
|
232
|
+
for (const [k, v] of Object.entries(value)) out[k] = scrubAny(v, role, `${fieldPath}.${k}`, policy, trustClass, ctx, logger);
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
function scrubProviderOptions(providerOptions, policy, trustClass, ctx, logger) {
|
|
238
|
+
if (providerOptions === void 0) return void 0;
|
|
239
|
+
const out = {};
|
|
240
|
+
for (const [k, v] of Object.entries(providerOptions)) out[k] = scrubAny(v, "provider-options", `providerOptions.${k}`, policy, trustClass, ctx, logger);
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
function detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger) {
|
|
244
|
+
if (!policy.detectSecretValue) return;
|
|
245
|
+
scrubAny(msg, msg.role, fieldPrefix, policy, trustClass, ctx, logger);
|
|
246
|
+
}
|
|
247
|
+
function reportViolation(policy, violation, logger) {
|
|
248
|
+
policy.onViolation?.(violation);
|
|
249
|
+
logger(`[graphorin/provider] withRedaction hit '${violation.patternName}' at '${violation.fieldPath}' (action: ${violation.action})`, {
|
|
250
|
+
patternName: violation.patternName,
|
|
251
|
+
fieldPath: violation.fieldPath,
|
|
252
|
+
role: violation.role,
|
|
253
|
+
matchLength: violation.matchLength,
|
|
254
|
+
trustClass: violation.trustClass
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function isSecretValue(value) {
|
|
258
|
+
if (value === null || typeof value !== "object") return false;
|
|
259
|
+
return value[SECRET_VALUE_BRAND] === true;
|
|
260
|
+
}
|
|
261
|
+
function inferTrustClass(acceptsSensitivity) {
|
|
262
|
+
if (acceptsSensitivity === void 0) return void 0;
|
|
263
|
+
const set = new Set(acceptsSensitivity);
|
|
264
|
+
if (set.has("secret")) return "loopback";
|
|
265
|
+
if (set.has("internal")) return "private";
|
|
266
|
+
if (set.has("public")) return "public-tls";
|
|
267
|
+
}
|
|
268
|
+
async function* scanStreamingResponse(source, policy, logger) {
|
|
269
|
+
const TAIL_WINDOW = 512;
|
|
270
|
+
let tail = "";
|
|
271
|
+
for await (const event of source) {
|
|
272
|
+
if (event.type === "text-delta" && policy.scanScope === "all") {
|
|
273
|
+
const haystack = tail + event.delta;
|
|
274
|
+
for (const pattern of policy.patterns) {
|
|
275
|
+
pattern.regex.lastIndex = 0;
|
|
276
|
+
let newMatchLength = 0;
|
|
277
|
+
let match = pattern.regex.exec(haystack);
|
|
278
|
+
while (match !== null) {
|
|
279
|
+
const endIndex = match.index + match[0].length;
|
|
280
|
+
if (match[0].length > 0 && endIndex > tail.length) newMatchLength += match[0].length;
|
|
281
|
+
if (!pattern.regex.global) break;
|
|
282
|
+
if (match[0].length === 0) pattern.regex.lastIndex += 1;
|
|
283
|
+
match = pattern.regex.exec(haystack);
|
|
284
|
+
}
|
|
285
|
+
if (newMatchLength > 0) reportViolation(policy, {
|
|
286
|
+
patternName: pattern.name,
|
|
287
|
+
fieldPath: "response.text-delta",
|
|
288
|
+
role: "assistant",
|
|
289
|
+
matchLength: newMatchLength,
|
|
290
|
+
action: "redact"
|
|
291
|
+
}, logger);
|
|
292
|
+
}
|
|
293
|
+
tail = haystack.length > TAIL_WINDOW ? haystack.slice(haystack.length - TAIL_WINDOW) : haystack;
|
|
294
|
+
}
|
|
295
|
+
yield event;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function defaultLogger(message, meta) {
|
|
299
|
+
if (meta !== void 0) console.warn(message, meta);
|
|
300
|
+
else console.warn(message);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
export { withRedaction };
|
|
305
|
+
//# sourceMappingURL=with-redaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-redaction.js","names":["TRUST_CLASS_DEFAULTS: Readonly<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>>","merged: RequiredPolicy","ctx: ScrubContext","next: AssistantMessage","out: Record<string, unknown>"],"sources":["../../src/middleware/with-redaction.ts"],"sourcesContent":["/**\n * `withRedaction` — outbound prompt-redaction middleware. Innermost\n * layer in the canonical order: it runs after every other middleware\n * has shaped the request and immediately before the underlying\n * provider call. The middleware:\n *\n * 1. Walks every text-bearing field of `ProviderRequest` (system /\n * user / assistant / tool messages, tool-call args, vendor-prefixed\n * `cache_control` spans).\n * 2. Detects `SecretValue`-shaped instances (cross-realm safe via\n * `Symbol.for('graphorin.SecretValue')`).\n * 3. Runs the configured pattern catalogue (defaults to the 14 built-in\n * patterns shared with the OTLP `RedactionValidator`).\n * 4. Applies the configured action (`'redact'` in-place by default;\n * `'throw'` for `failClosed: true` deployments).\n * 5. Strips Anthropic-shape `cache_control` markers from any span\n * that hit a pattern (default `stripCacheControlOnHit: true`).\n * 6. Emits one counter increment per detection plus a sanitised audit\n * record (audit emission is delegated to the consumer via\n * `onViolation`).\n * 7. Optionally scans streamed `text-delta` chunks for the same\n * patterns — observability-only in v0.1, no stream mutation.\n *\n * @packageDocumentation\n */\n\nimport type {\n AssistantMessage,\n LocalProviderTrust,\n Message,\n Provider,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n ToolMessage,\n UserMessage,\n} from '@graphorin/core';\nimport { SECRET_VALUE_BRAND } from '@graphorin/core';\nimport {\n BUILT_IN_PATTERNS,\n type RedactionPattern,\n} from '@graphorin/observability/redaction/patterns';\nimport { PromptRedactionError } from '../errors/errors.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Range of fields scanned by the middleware.\n *\n * @stable\n */\nexport type PromptRedactionScanScope = 'all' | 'untrusted' | 'secret-value-only';\n\n/**\n * Sanitized record handed to `onViolation`. Carries metadata only;\n * never the matched value.\n *\n * @stable\n */\nexport interface PromptRedactionViolation {\n readonly patternName: string;\n readonly fieldPath: string;\n readonly role?: string;\n readonly matchLength: number;\n readonly trustClass?: LocalProviderTrust;\n readonly action: 'redact' | 'throw' | 'block-and-prompt-user';\n}\n\n/**\n * Mutable per-request scrubbing context. Tracks whether any pattern\n * matched so the post-scrub `cache_control` strip can decide whether\n * to fire.\n *\n * @internal\n */\ninterface ScrubContext {\n hits: number;\n cacheControlHits: number;\n}\n\n/**\n * Full prompt-redaction policy.\n *\n * @stable\n */\nexport interface PromptRedactionPolicy {\n /** Pattern catalogue. Defaults to the 14 built-in patterns. */\n readonly patterns?: ReadonlyArray<RedactionPattern>;\n /** Action on detection. Defaults to `'redact'`. */\n readonly action?: 'redact' | 'throw' | 'block-and-prompt-user';\n /** Throw on the first hit instead of redacting in-place. */\n readonly failClosed?: boolean;\n /** Range of fields scanned. Defaults to `'all'`. */\n readonly scanScope?: PromptRedactionScanScope;\n /** Detect `SecretValue` instances anywhere in the request. */\n readonly detectSecretValue?: boolean;\n /** Strip Anthropic-shape `cache_control` markers on hit. */\n readonly stripCacheControlOnHit?: boolean;\n /** Per-trust-class override block. */\n readonly byTrustClass?: Partial<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>>;\n /** Sanitised violation hook (audit emission lives downstream). */\n readonly onViolation?: (violation: PromptRedactionViolation) => void;\n /** Optional logger override. Defaults to `console.warn`. */\n readonly logger?: (message: string, meta?: object) => void;\n /** Test hook — synthetic trust class. */\n readonly trustClassOverride?: LocalProviderTrust;\n}\n\nconst TRUST_CLASS_DEFAULTS: Readonly<Record<LocalProviderTrust, Partial<PromptRedactionPolicy>>> =\n Object.freeze({\n loopback: { scanScope: 'secret-value-only' },\n private: { scanScope: 'all', failClosed: false },\n 'public-tls': { scanScope: 'all', failClosed: false },\n 'public-cleartext': {},\n });\n\n/**\n * @stable\n */\nexport const withRedaction = defineProviderMiddleware<PromptRedactionPolicy>({\n kind: 'withRedaction',\n factory: (rawPolicy: PromptRedactionPolicy) => {\n const baseLogger = rawPolicy.logger ?? defaultLogger;\n return (next: Provider): Provider => {\n const trustClass = rawPolicy.trustClassOverride ?? inferTrustClass(next.acceptsSensitivity);\n const policy = effectivePolicy(rawPolicy, trustClass);\n return {\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);\n return scanStreamingResponse(next.stream(scrubbed), policy, baseLogger);\n },\n async generate(req) {\n const scrubbed = scrubRequest(req, policy, trustClass, baseLogger);\n return next.generate(scrubbed);\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n };\n };\n },\n});\n\nfunction effectivePolicy(\n raw: PromptRedactionPolicy,\n trustClass: LocalProviderTrust | undefined,\n): RequiredPolicy {\n const tierDefaults = trustClass !== undefined ? TRUST_CLASS_DEFAULTS[trustClass] : {};\n const explicitOverride = trustClass !== undefined ? (raw.byTrustClass?.[trustClass] ?? {}) : {};\n const merged: RequiredPolicy = {\n patterns: ensureGlobalPatterns(\n raw.patterns ?? explicitOverride.patterns ?? tierDefaults.patterns ?? BUILT_IN_PATTERNS,\n ),\n action: raw.action ?? explicitOverride.action ?? tierDefaults.action ?? 'redact',\n failClosed: pick(raw.failClosed, explicitOverride.failClosed, tierDefaults.failClosed, false),\n scanScope: raw.scanScope ?? explicitOverride.scanScope ?? tierDefaults.scanScope ?? 'all',\n detectSecretValue: pick(\n raw.detectSecretValue,\n explicitOverride.detectSecretValue,\n tierDefaults.detectSecretValue,\n true,\n ),\n stripCacheControlOnHit: pick(\n raw.stripCacheControlOnHit,\n explicitOverride.stripCacheControlOnHit,\n tierDefaults.stripCacheControlOnHit,\n true,\n ),\n onViolation: raw.onViolation,\n };\n if (raw.failClosed === true || explicitOverride.failClosed === true) {\n merged.action = 'throw';\n }\n return merged;\n}\n\n/**\n * PS-22: callers can supply a pattern without the global flag. `.replace` /\n * `.match` then stop after the FIRST occurrence, so a second secret on the same\n * line is neither redacted nor counted. Re-compile every non-global pattern\n * with the `g` flag so all occurrences are covered. The 14 built-ins already\n * carry `/g`; this only rescues user-supplied patterns (identity otherwise).\n */\nfunction ensureGlobalPatterns(\n patterns: ReadonlyArray<RedactionPattern>,\n): ReadonlyArray<RedactionPattern> {\n let changed = false;\n const out = patterns.map((p) => {\n if (p.regex.global) return p;\n changed = true;\n return { ...p, regex: new RegExp(p.regex.source, `${p.regex.flags}g`) };\n });\n return changed ? out : patterns;\n}\n\nfunction pick<T>(...values: ReadonlyArray<T | undefined>): T {\n for (const v of values) {\n if (v !== undefined) return v as T;\n }\n // The last argument acts as the default and is always defined per\n // the call sites above.\n return values[values.length - 1] as T;\n}\n\ninterface RequiredPolicy {\n patterns: ReadonlyArray<RedactionPattern>;\n action: 'redact' | 'throw' | 'block-and-prompt-user';\n failClosed: boolean;\n scanScope: PromptRedactionScanScope;\n detectSecretValue: boolean;\n stripCacheControlOnHit: boolean;\n onViolation: ((violation: PromptRedactionViolation) => void) | undefined;\n}\n\nfunction scrubRequest(\n req: ProviderRequest,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n logger: (message: string, meta?: object) => void,\n): ProviderRequest {\n const ctx: ScrubContext = { hits: 0, cacheControlHits: 0 };\n const messages = req.messages.map((msg, i) =>\n scrubMessage(msg, policy, trustClass, `messages[${i}]`, ctx, logger),\n );\n const systemMessage =\n req.systemMessage !== undefined\n ? scrubText(req.systemMessage, 'system', 'systemMessage', policy, trustClass, ctx, logger)\n : undefined;\n let providerOptions = scrubProviderOptions(req.providerOptions, policy, trustClass, ctx, logger);\n if (\n policy.stripCacheControlOnHit &&\n ctx.hits > 0 &&\n providerOptions !== undefined &&\n hasAnthropicCacheControl(providerOptions)\n ) {\n providerOptions = stripAnthropicCacheControl(providerOptions);\n reportViolation(\n policy,\n {\n patternName: 'anthropic-cache-control-stripped',\n fieldPath: 'providerOptions.anthropic.cache_control',\n matchLength: 0,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: 'redact',\n },\n logger,\n );\n ctx.cacheControlHits++;\n }\n return {\n ...req,\n messages,\n ...(systemMessage !== undefined ? { systemMessage } : {}),\n ...(providerOptions !== undefined ? { providerOptions } : {}),\n };\n}\n\nfunction hasAnthropicCacheControl(providerOptions: Record<string, unknown>): boolean {\n const anthropic = providerOptions.anthropic;\n if (anthropic === null || typeof anthropic !== 'object') return false;\n return 'cache_control' in (anthropic as Record<string, unknown>);\n}\n\nfunction stripAnthropicCacheControl(\n providerOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const anthropic = providerOptions.anthropic;\n if (anthropic === null || typeof anthropic !== 'object') return providerOptions;\n const { cache_control: _drop, ...rest } = anthropic as Record<string, unknown>;\n void _drop;\n return { ...providerOptions, anthropic: rest };\n}\n\nfunction scrubMessage(\n msg: Message,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): Message {\n if (policy.scanScope === 'secret-value-only') {\n detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);\n return msg;\n }\n if (policy.scanScope === 'untrusted' && msg.role === 'system') {\n detectSecretValuesIn(msg, policy, trustClass, fieldPrefix, ctx, logger);\n return msg;\n }\n switch (msg.role) {\n case 'system':\n return {\n role: 'system',\n content: scrubText(\n msg.content,\n 'system',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n case 'user':\n return scrubUserMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n case 'assistant':\n return scrubAssistantMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n case 'tool':\n return scrubToolMessage(msg, policy, trustClass, fieldPrefix, ctx, logger);\n default:\n return msg;\n }\n}\n\nfunction scrubUserMessage(\n msg: UserMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): UserMessage {\n const next: UserMessage = {\n role: 'user',\n content: scrubContent(\n msg.content,\n 'user',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as UserMessage['content'],\n ...(msg.userId !== undefined ? { userId: msg.userId } : {}),\n };\n return next;\n}\n\nfunction scrubAssistantMessage(\n msg: AssistantMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): AssistantMessage {\n const next: AssistantMessage = {\n role: 'assistant',\n content: scrubContent(\n msg.content,\n 'assistant',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as AssistantMessage['content'],\n ...(msg.agentId !== undefined ? { agentId: msg.agentId } : {}),\n };\n if (msg.toolCalls !== undefined) {\n (next as { toolCalls?: AssistantMessage['toolCalls'] }).toolCalls = msg.toolCalls.map(\n (tc, idx) => ({\n ...tc,\n args: scrubAny(\n tc.args,\n 'assistant',\n `${fieldPrefix}.toolCalls[${idx}].args`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n }),\n );\n }\n return next;\n}\n\nfunction scrubToolMessage(\n msg: ToolMessage,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): ToolMessage {\n return {\n role: 'tool',\n toolCallId: msg.toolCallId,\n content: scrubContent(\n msg.content,\n 'tool',\n `${fieldPrefix}.content`,\n policy,\n trustClass,\n ctx,\n logger,\n ) as ToolMessage['content'],\n };\n}\n\nfunction scrubContent(\n content: string | ReadonlyArray<unknown>,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): string | ReadonlyArray<unknown> {\n if (typeof content === 'string') {\n return scrubText(content, role, fieldPath, policy, trustClass, ctx, logger);\n }\n return content.map((part, idx) => {\n if (typeof part === 'string') {\n return scrubText(part, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger);\n }\n if (part === null || typeof part !== 'object') return part;\n const partWithType = part as { type?: string; text?: string };\n if (partWithType.type === 'text' && typeof partWithType.text === 'string') {\n return {\n ...part,\n text: scrubText(\n partWithType.text,\n role,\n `${fieldPath}[${idx}].text`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n }\n if (partWithType.type === 'reasoning' && typeof partWithType.text === 'string') {\n return {\n ...part,\n text: scrubText(\n partWithType.text,\n role,\n `${fieldPath}[${idx}].text`,\n policy,\n trustClass,\n ctx,\n logger,\n ),\n };\n }\n return part;\n });\n}\n\nfunction scrubText(\n text: string,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): string {\n let scrubbed = text;\n for (const pattern of policy.patterns) {\n pattern.regex.lastIndex = 0;\n if (pattern.regex.test(scrubbed)) {\n const matches = scrubbed.match(pattern.regex) ?? [];\n const matchLength = matches.reduce((sum, m) => sum + m.length, 0);\n ctx.hits++;\n reportViolation(\n policy,\n {\n patternName: pattern.name,\n fieldPath,\n role,\n matchLength,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: policy.action,\n },\n logger,\n );\n if (policy.action === 'throw' || policy.failClosed) {\n throw new PromptRedactionError({\n patternName: pattern.name,\n fieldPath,\n ...(role !== undefined ? { role } : {}),\n });\n }\n pattern.regex.lastIndex = 0;\n scrubbed = scrubbed.replace(pattern.regex, pattern.mask ?? `[REDACTED ${pattern.name}]`);\n }\n }\n return scrubbed;\n}\n\nfunction scrubAny(\n value: unknown,\n role: string,\n fieldPath: string,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): unknown {\n if (typeof value === 'string') {\n return scrubText(value, role, fieldPath, policy, trustClass, ctx, logger);\n }\n if (value === null || value === undefined) return value;\n if (Array.isArray(value)) {\n return value.map((item, idx) =>\n scrubAny(item, role, `${fieldPath}[${idx}]`, policy, trustClass, ctx, logger),\n );\n }\n if (typeof value === 'object') {\n if (policy.detectSecretValue && isSecretValue(value)) {\n ctx.hits++;\n reportViolation(\n policy,\n {\n patternName: 'graphorin-secret-value',\n fieldPath,\n role,\n matchLength: 0,\n ...(trustClass !== undefined ? { trustClass } : {}),\n action: policy.action,\n },\n logger,\n );\n return '[REDACTED graphorin-secret-value]';\n }\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = scrubAny(v, role, `${fieldPath}.${k}`, policy, trustClass, ctx, logger);\n }\n return out;\n }\n return value;\n}\n\nfunction scrubProviderOptions(\n providerOptions: Readonly<Record<string, unknown>> | undefined,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): Record<string, unknown> | undefined {\n if (providerOptions === undefined) return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(providerOptions)) {\n out[k] = scrubAny(\n v,\n 'provider-options',\n `providerOptions.${k}`,\n policy,\n trustClass,\n ctx,\n logger,\n );\n }\n return out;\n}\n\nfunction detectSecretValuesIn(\n msg: Message,\n policy: RequiredPolicy,\n trustClass: LocalProviderTrust | undefined,\n fieldPrefix: string,\n ctx: ScrubContext,\n logger: (message: string, meta?: object) => void,\n): void {\n if (!policy.detectSecretValue) return;\n scrubAny(msg, msg.role, fieldPrefix, policy, trustClass, ctx, logger);\n}\n\nfunction reportViolation(\n policy: RequiredPolicy,\n violation: PromptRedactionViolation,\n logger: (message: string, meta?: object) => void,\n): void {\n policy.onViolation?.(violation);\n logger(\n `[graphorin/provider] withRedaction hit '${violation.patternName}' at '${violation.fieldPath}' (action: ${violation.action})`,\n {\n patternName: violation.patternName,\n fieldPath: violation.fieldPath,\n role: violation.role,\n matchLength: violation.matchLength,\n trustClass: violation.trustClass,\n },\n );\n}\n\nfunction isSecretValue(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n return (value as Record<string | symbol, unknown>)[SECRET_VALUE_BRAND] === true;\n}\n\nfunction inferTrustClass(\n acceptsSensitivity: ReadonlyArray<Sensitivity> | undefined,\n): LocalProviderTrust | undefined {\n if (acceptsSensitivity === undefined) return undefined;\n const set = new Set<Sensitivity>(acceptsSensitivity);\n if (set.has('secret')) return 'loopback';\n if (set.has('internal')) return 'private';\n if (set.has('public')) return 'public-tls';\n return undefined;\n}\n\nasync function* scanStreamingResponse(\n source: AsyncIterable<ProviderEvent>,\n policy: RequiredPolicy,\n logger: (message: string, meta?: object) => void,\n): AsyncIterable<ProviderEvent> {\n // PS-22: secrets can straddle text-delta boundaries. Keep a bounded tail of\n // recently-streamed text and scan `tail + delta`, reporting only matches that\n // reach into the freshly-arrived delta so a prior window's matches are not\n // double-counted. The tail is capped (no unbounded accumulation); a single\n // match longer than the cap can still slip a boundary (best-effort).\n const TAIL_WINDOW = 512;\n let tail = '';\n for await (const event of source) {\n if (event.type === 'text-delta' && policy.scanScope === 'all') {\n // Observability-only — match patterns and emit synthetic violation rows;\n // do NOT mutate the stream content (mid-stream mutation would break\n // structured-output / tool-call parsing).\n const haystack = tail + event.delta;\n for (const pattern of policy.patterns) {\n pattern.regex.lastIndex = 0;\n let newMatchLength = 0;\n let match = pattern.regex.exec(haystack);\n while (match !== null) {\n const endIndex = match.index + match[0].length;\n // A match wholly inside `tail` was reported on the prior delta.\n if (match[0].length > 0 && endIndex > tail.length) {\n newMatchLength += match[0].length;\n }\n if (!pattern.regex.global) break;\n if (match[0].length === 0) pattern.regex.lastIndex += 1;\n match = pattern.regex.exec(haystack);\n }\n if (newMatchLength > 0) {\n reportViolation(\n policy,\n {\n patternName: pattern.name,\n fieldPath: 'response.text-delta',\n role: 'assistant',\n matchLength: newMatchLength,\n action: 'redact',\n },\n logger,\n );\n }\n }\n tail =\n haystack.length > TAIL_WINDOW ? haystack.slice(haystack.length - TAIL_WINDOW) : haystack;\n }\n yield event;\n }\n}\n\nfunction defaultLogger(message: string, meta?: object): void {\n if (meta !== undefined) console.warn(message, meta);\n else console.warn(message);\n}\n\n// Re-export to silence \"declared but unused\" lints for downstream\n// consumers that import only the types.\nexport type _RedactionResponseGuard = ProviderResponse;\n"],"mappings":";;;;;;AA4GA,MAAMA,uBACJ,OAAO,OAAO;CACZ,UAAU,EAAE,WAAW,qBAAqB;CAC5C,SAAS;EAAE,WAAW;EAAO,YAAY;EAAO;CAChD,cAAc;EAAE,WAAW;EAAO,YAAY;EAAO;CACrD,oBAAoB,EAAE;CACvB,CAAC;;;;AAKJ,MAAa,gBAAgB,yBAAgD;CAC3E,MAAM;CACN,UAAU,cAAqC;EAC7C,MAAM,aAAa,UAAU,UAAU;AACvC,UAAQ,SAA6B;GACnC,MAAM,aAAa,UAAU,sBAAsB,gBAAgB,KAAK,mBAAmB;GAC3F,MAAM,SAAS,gBAAgB,WAAW,WAAW;AACrD,UAAO;IACL,MAAM,KAAK;IACX,SAAS,KAAK;IACd,cAAc,KAAK;IACnB,GAAI,KAAK,uBAAuB,SAC5B,EAAE,oBAAoB,KAAK,oBAAoB,GAC/C,EAAE;IACN,OAAO,KAAK;KACV,MAAM,WAAW,aAAa,KAAK,QAAQ,YAAY,WAAW;AAClE,YAAO,sBAAsB,KAAK,OAAO,SAAS,EAAE,QAAQ,WAAW;;IAEzE,MAAM,SAAS,KAAK;KAClB,MAAM,WAAW,aAAa,KAAK,QAAQ,YAAY,WAAW;AAClE,YAAO,KAAK,SAAS,SAAS;;IAEhC,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;IACzE;;;CAGN,CAAC;AAEF,SAAS,gBACP,KACA,YACgB;CAChB,MAAM,eAAe,eAAe,SAAY,qBAAqB,cAAc,EAAE;CACrF,MAAM,mBAAmB,eAAe,SAAa,IAAI,eAAe,eAAe,EAAE,GAAI,EAAE;CAC/F,MAAMC,SAAyB;EAC7B,UAAU,qBACR,IAAI,YAAY,iBAAiB,YAAY,aAAa,YAAY,kBACvE;EACD,QAAQ,IAAI,UAAU,iBAAiB,UAAU,aAAa,UAAU;EACxE,YAAY,KAAK,IAAI,YAAY,iBAAiB,YAAY,aAAa,YAAY,MAAM;EAC7F,WAAW,IAAI,aAAa,iBAAiB,aAAa,aAAa,aAAa;EACpF,mBAAmB,KACjB,IAAI,mBACJ,iBAAiB,mBACjB,aAAa,mBACb,KACD;EACD,wBAAwB,KACtB,IAAI,wBACJ,iBAAiB,wBACjB,aAAa,wBACb,KACD;EACD,aAAa,IAAI;EAClB;AACD,KAAI,IAAI,eAAe,QAAQ,iBAAiB,eAAe,KAC7D,QAAO,SAAS;AAElB,QAAO;;;;;;;;;AAUT,SAAS,qBACP,UACiC;CACjC,IAAI,UAAU;CACd,MAAM,MAAM,SAAS,KAAK,MAAM;AAC9B,MAAI,EAAE,MAAM,OAAQ,QAAO;AAC3B,YAAU;AACV,SAAO;GAAE,GAAG;GAAG,OAAO,IAAI,OAAO,EAAE,MAAM,QAAQ,GAAG,EAAE,MAAM,MAAM,GAAG;GAAE;GACvE;AACF,QAAO,UAAU,MAAM;;AAGzB,SAAS,KAAQ,GAAG,QAAyC;AAC3D,MAAK,MAAM,KAAK,OACd,KAAI,MAAM,OAAW,QAAO;AAI9B,QAAO,OAAO,OAAO,SAAS;;AAahC,SAAS,aACP,KACA,QACA,YACA,QACiB;CACjB,MAAMC,MAAoB;EAAE,MAAM;EAAG,kBAAkB;EAAG;CAC1D,MAAM,WAAW,IAAI,SAAS,KAAK,KAAK,MACtC,aAAa,KAAK,QAAQ,YAAY,YAAY,EAAE,IAAI,KAAK,OAAO,CACrE;CACD,MAAM,gBACJ,IAAI,kBAAkB,SAClB,UAAU,IAAI,eAAe,UAAU,iBAAiB,QAAQ,YAAY,KAAK,OAAO,GACxF;CACN,IAAI,kBAAkB,qBAAqB,IAAI,iBAAiB,QAAQ,YAAY,KAAK,OAAO;AAChG,KACE,OAAO,0BACP,IAAI,OAAO,KACX,oBAAoB,UACpB,yBAAyB,gBAAgB,EACzC;AACA,oBAAkB,2BAA2B,gBAAgB;AAC7D,kBACE,QACA;GACE,aAAa;GACb,WAAW;GACX,aAAa;GACb,GAAI,eAAe,SAAY,EAAE,YAAY,GAAG,EAAE;GAClD,QAAQ;GACT,EACD,OACD;AACD,MAAI;;AAEN,QAAO;EACL,GAAG;EACH;EACA,GAAI,kBAAkB,SAAY,EAAE,eAAe,GAAG,EAAE;EACxD,GAAI,oBAAoB,SAAY,EAAE,iBAAiB,GAAG,EAAE;EAC7D;;AAGH,SAAS,yBAAyB,iBAAmD;CACnF,MAAM,YAAY,gBAAgB;AAClC,KAAI,cAAc,QAAQ,OAAO,cAAc,SAAU,QAAO;AAChE,QAAO,mBAAoB;;AAG7B,SAAS,2BACP,iBACyB;CACzB,MAAM,YAAY,gBAAgB;AAClC,KAAI,cAAc,QAAQ,OAAO,cAAc,SAAU,QAAO;CAChE,MAAM,EAAE,eAAe,OAAO,GAAG,SAAS;AAE1C,QAAO;EAAE,GAAG;EAAiB,WAAW;EAAM;;AAGhD,SAAS,aACP,KACA,QACA,YACA,aACA,KACA,QACS;AACT,KAAI,OAAO,cAAc,qBAAqB;AAC5C,uBAAqB,KAAK,QAAQ,YAAY,aAAa,KAAK,OAAO;AACvE,SAAO;;AAET,KAAI,OAAO,cAAc,eAAe,IAAI,SAAS,UAAU;AAC7D,uBAAqB,KAAK,QAAQ,YAAY,aAAa,KAAK,OAAO;AACvE,SAAO;;AAET,SAAQ,IAAI,MAAZ;EACE,KAAK,SACH,QAAO;GACL,MAAM;GACN,SAAS,UACP,IAAI,SACJ,UACA,GAAG,YAAY,WACf,QACA,YACA,KACA,OACD;GACF;EACH,KAAK,OACH,QAAO,iBAAiB,KAAK,QAAQ,YAAY,aAAa,KAAK,OAAO;EAC5E,KAAK,YACH,QAAO,sBAAsB,KAAK,QAAQ,YAAY,aAAa,KAAK,OAAO;EACjF,KAAK,OACH,QAAO,iBAAiB,KAAK,QAAQ,YAAY,aAAa,KAAK,OAAO;EAC5E,QACE,QAAO;;;AAIb,SAAS,iBACP,KACA,QACA,YACA,aACA,KACA,QACa;AAcb,QAb0B;EACxB,MAAM;EACN,SAAS,aACP,IAAI,SACJ,QACA,GAAG,YAAY,WACf,QACA,YACA,KACA,OACD;EACD,GAAI,IAAI,WAAW,SAAY,EAAE,QAAQ,IAAI,QAAQ,GAAG,EAAE;EAC3D;;AAIH,SAAS,sBACP,KACA,QACA,YACA,aACA,KACA,QACkB;CAClB,MAAMC,OAAyB;EAC7B,MAAM;EACN,SAAS,aACP,IAAI,SACJ,aACA,GAAG,YAAY,WACf,QACA,YACA,KACA,OACD;EACD,GAAI,IAAI,YAAY,SAAY,EAAE,SAAS,IAAI,SAAS,GAAG,EAAE;EAC9D;AACD,KAAI,IAAI,cAAc,OACpB,CAAC,KAAuD,YAAY,IAAI,UAAU,KAC/E,IAAI,SAAS;EACZ,GAAG;EACH,MAAM,SACJ,GAAG,MACH,aACA,GAAG,YAAY,aAAa,IAAI,SAChC,QACA,YACA,KACA,OACD;EACF,EACF;AAEH,QAAO;;AAGT,SAAS,iBACP,KACA,QACA,YACA,aACA,KACA,QACa;AACb,QAAO;EACL,MAAM;EACN,YAAY,IAAI;EAChB,SAAS,aACP,IAAI,SACJ,QACA,GAAG,YAAY,WACf,QACA,YACA,KACA,OACD;EACF;;AAGH,SAAS,aACP,SACA,MACA,WACA,QACA,YACA,KACA,QACiC;AACjC,KAAI,OAAO,YAAY,SACrB,QAAO,UAAU,SAAS,MAAM,WAAW,QAAQ,YAAY,KAAK,OAAO;AAE7E,QAAO,QAAQ,KAAK,MAAM,QAAQ;AAChC,MAAI,OAAO,SAAS,SAClB,QAAO,UAAU,MAAM,MAAM,GAAG,UAAU,GAAG,IAAI,IAAI,QAAQ,YAAY,KAAK,OAAO;AAEvF,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;EACtD,MAAM,eAAe;AACrB,MAAI,aAAa,SAAS,UAAU,OAAO,aAAa,SAAS,SAC/D,QAAO;GACL,GAAG;GACH,MAAM,UACJ,aAAa,MACb,MACA,GAAG,UAAU,GAAG,IAAI,SACpB,QACA,YACA,KACA,OACD;GACF;AAEH,MAAI,aAAa,SAAS,eAAe,OAAO,aAAa,SAAS,SACpE,QAAO;GACL,GAAG;GACH,MAAM,UACJ,aAAa,MACb,MACA,GAAG,UAAU,GAAG,IAAI,SACpB,QACA,YACA,KACA,OACD;GACF;AAEH,SAAO;GACP;;AAGJ,SAAS,UACP,MACA,MACA,WACA,QACA,YACA,KACA,QACQ;CACR,IAAI,WAAW;AACf,MAAK,MAAM,WAAW,OAAO,UAAU;AACrC,UAAQ,MAAM,YAAY;AAC1B,MAAI,QAAQ,MAAM,KAAK,SAAS,EAAE;GAEhC,MAAM,eADU,SAAS,MAAM,QAAQ,MAAM,IAAI,EAAE,EACvB,QAAQ,KAAK,MAAM,MAAM,EAAE,QAAQ,EAAE;AACjE,OAAI;AACJ,mBACE,QACA;IACE,aAAa,QAAQ;IACrB;IACA;IACA;IACA,GAAI,eAAe,SAAY,EAAE,YAAY,GAAG,EAAE;IAClD,QAAQ,OAAO;IAChB,EACD,OACD;AACD,OAAI,OAAO,WAAW,WAAW,OAAO,WACtC,OAAM,IAAI,qBAAqB;IAC7B,aAAa,QAAQ;IACrB;IACA,GAAI,SAAS,SAAY,EAAE,MAAM,GAAG,EAAE;IACvC,CAAC;AAEJ,WAAQ,MAAM,YAAY;AAC1B,cAAW,SAAS,QAAQ,QAAQ,OAAO,QAAQ,QAAQ,aAAa,QAAQ,KAAK,GAAG;;;AAG5F,QAAO;;AAGT,SAAS,SACP,OACA,MACA,WACA,QACA,YACA,KACA,QACS;AACT,KAAI,OAAO,UAAU,SACnB,QAAO,UAAU,OAAO,MAAM,WAAW,QAAQ,YAAY,KAAK,OAAO;AAE3E,KAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,MAAM,QACtB,SAAS,MAAM,MAAM,GAAG,UAAU,GAAG,IAAI,IAAI,QAAQ,YAAY,KAAK,OAAO,CAC9E;AAEH,KAAI,OAAO,UAAU,UAAU;AAC7B,MAAI,OAAO,qBAAqB,cAAc,MAAM,EAAE;AACpD,OAAI;AACJ,mBACE,QACA;IACE,aAAa;IACb;IACA;IACA,aAAa;IACb,GAAI,eAAe,SAAY,EAAE,YAAY,GAAG,EAAE;IAClD,QAAQ,OAAO;IAChB,EACD,OACD;AACD,UAAO;;EAET,MAAMC,MAA+B,EAAE;AACvC,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAiC,CACnE,KAAI,KAAK,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,QAAQ,YAAY,KAAK,OAAO;AAElF,SAAO;;AAET,QAAO;;AAGT,SAAS,qBACP,iBACA,QACA,YACA,KACA,QACqC;AACrC,KAAI,oBAAoB,OAAW,QAAO;CAC1C,MAAMA,MAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,gBAAgB,CAClD,KAAI,KAAK,SACP,GACA,oBACA,mBAAmB,KACnB,QACA,YACA,KACA,OACD;AAEH,QAAO;;AAGT,SAAS,qBACP,KACA,QACA,YACA,aACA,KACA,QACM;AACN,KAAI,CAAC,OAAO,kBAAmB;AAC/B,UAAS,KAAK,IAAI,MAAM,aAAa,QAAQ,YAAY,KAAK,OAAO;;AAGvE,SAAS,gBACP,QACA,WACA,QACM;AACN,QAAO,cAAc,UAAU;AAC/B,QACE,2CAA2C,UAAU,YAAY,QAAQ,UAAU,UAAU,aAAa,UAAU,OAAO,IAC3H;EACE,aAAa,UAAU;EACvB,WAAW,UAAU;EACrB,MAAM,UAAU;EAChB,aAAa,UAAU;EACvB,YAAY,UAAU;EACvB,CACF;;AAGH,SAAS,cAAc,OAAyB;AAC9C,KAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAQ,MAA2C,wBAAwB;;AAG7E,SAAS,gBACP,oBACgC;AAChC,KAAI,uBAAuB,OAAW,QAAO;CAC7C,MAAM,MAAM,IAAI,IAAiB,mBAAmB;AACpD,KAAI,IAAI,IAAI,SAAS,CAAE,QAAO;AAC9B,KAAI,IAAI,IAAI,WAAW,CAAE,QAAO;AAChC,KAAI,IAAI,IAAI,SAAS,CAAE,QAAO;;AAIhC,gBAAgB,sBACd,QACA,QACA,QAC8B;CAM9B,MAAM,cAAc;CACpB,IAAI,OAAO;AACX,YAAW,MAAM,SAAS,QAAQ;AAChC,MAAI,MAAM,SAAS,gBAAgB,OAAO,cAAc,OAAO;GAI7D,MAAM,WAAW,OAAO,MAAM;AAC9B,QAAK,MAAM,WAAW,OAAO,UAAU;AACrC,YAAQ,MAAM,YAAY;IAC1B,IAAI,iBAAiB;IACrB,IAAI,QAAQ,QAAQ,MAAM,KAAK,SAAS;AACxC,WAAO,UAAU,MAAM;KACrB,MAAM,WAAW,MAAM,QAAQ,MAAM,GAAG;AAExC,SAAI,MAAM,GAAG,SAAS,KAAK,WAAW,KAAK,OACzC,mBAAkB,MAAM,GAAG;AAE7B,SAAI,CAAC,QAAQ,MAAM,OAAQ;AAC3B,SAAI,MAAM,GAAG,WAAW,EAAG,SAAQ,MAAM,aAAa;AACtD,aAAQ,QAAQ,MAAM,KAAK,SAAS;;AAEtC,QAAI,iBAAiB,EACnB,iBACE,QACA;KACE,aAAa,QAAQ;KACrB,WAAW;KACX,MAAM;KACN,aAAa;KACb,QAAQ;KACT,EACD,OACD;;AAGL,UACE,SAAS,SAAS,cAAc,SAAS,MAAM,SAAS,SAAS,YAAY,GAAG;;AAEpF,QAAM;;;AAIV,SAAS,cAAc,SAAiB,MAAqB;AAC3D,KAAI,SAAS,OAAW,SAAQ,KAAK,SAAS,KAAK;KAC9C,SAAQ,KAAK,QAAQ"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as _graphorin_core6 from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/middleware/with-retry.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for {@link withRetry}.
|
|
7
|
+
*
|
|
8
|
+
* @stable
|
|
9
|
+
*/
|
|
10
|
+
interface WithRetryOptions {
|
|
11
|
+
readonly maxRetries?: number;
|
|
12
|
+
readonly backoff?: 'exponential' | 'linear' | 'constant';
|
|
13
|
+
readonly initialDelayMs?: number;
|
|
14
|
+
readonly maxDelayMs?: number;
|
|
15
|
+
readonly jitter?: boolean;
|
|
16
|
+
readonly retryableErrors?: (err: unknown) => boolean;
|
|
17
|
+
/** Optional sleep override (test fixtures use a synchronous resolver). */
|
|
18
|
+
readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @stable
|
|
22
|
+
*/
|
|
23
|
+
declare const withRetry: (opts: WithRetryOptions) => _graphorin_core6.ProviderMiddleware;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { WithRetryOptions, withRetry };
|
|
26
|
+
//# sourceMappingURL=with-retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-retry.d.ts","names":[],"sources":["../../src/middleware/with-retry.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAmBiB,gBAAA;;;;;;;;6CAQ4B,gBAAgB;;;;;cAMhD,kBAAS,qBAAA,gBAAA,CAAA"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { defineProviderMiddleware } from "./compose.js";
|
|
2
|
+
import { isAbortError } from "../internal/abort.js";
|
|
3
|
+
|
|
4
|
+
//#region src/middleware/with-retry.ts
|
|
5
|
+
/**
|
|
6
|
+
* @stable
|
|
7
|
+
*/
|
|
8
|
+
const withRetry = defineProviderMiddleware({
|
|
9
|
+
kind: "withRetry",
|
|
10
|
+
factory: (opts) => {
|
|
11
|
+
const maxRetries = opts.maxRetries ?? 3;
|
|
12
|
+
const backoff = opts.backoff ?? "exponential";
|
|
13
|
+
const initialDelay = opts.initialDelayMs ?? 250;
|
|
14
|
+
const maxDelay = opts.maxDelayMs ?? 3e4;
|
|
15
|
+
const jitter = opts.jitter ?? true;
|
|
16
|
+
const isRetryable = opts.retryableErrors ?? defaultRetryable;
|
|
17
|
+
const sleep = opts.sleepImpl ?? defaultSleep;
|
|
18
|
+
return (next) => ({
|
|
19
|
+
name: next.name,
|
|
20
|
+
modelId: next.modelId,
|
|
21
|
+
capabilities: next.capabilities,
|
|
22
|
+
...next.acceptsSensitivity !== void 0 ? { acceptsSensitivity: next.acceptsSensitivity } : {},
|
|
23
|
+
stream(req) {
|
|
24
|
+
return retryingStream(next, req, maxRetries, backoff, initialDelay, maxDelay, jitter, isRetryable, sleep);
|
|
25
|
+
},
|
|
26
|
+
async generate(req) {
|
|
27
|
+
let attempt = 0;
|
|
28
|
+
for (;;) try {
|
|
29
|
+
return await next.generate(req);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (req.signal?.aborted) throw err;
|
|
32
|
+
if (attempt >= maxRetries) throw err;
|
|
33
|
+
if (!isRetryable(err)) throw err;
|
|
34
|
+
const hint = readRetryAfter(err);
|
|
35
|
+
await sleep(hint !== null ? Math.min(hint, maxDelay) : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter), req.signal);
|
|
36
|
+
attempt++;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
...next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
async function* retryingStream(next, req, maxRetries, backoff, initialDelay, maxDelay, jitter, isRetryable, sleep) {
|
|
44
|
+
let attempt = 0;
|
|
45
|
+
let yieldedAny = false;
|
|
46
|
+
for (;;) try {
|
|
47
|
+
for await (const event of next.stream(req)) {
|
|
48
|
+
yieldedAny = true;
|
|
49
|
+
yield event;
|
|
50
|
+
}
|
|
51
|
+
if (yieldedAny) return;
|
|
52
|
+
yield {
|
|
53
|
+
type: "finish",
|
|
54
|
+
finishReason: "stop",
|
|
55
|
+
usage: {
|
|
56
|
+
promptTokens: 0,
|
|
57
|
+
completionTokens: 0,
|
|
58
|
+
totalTokens: 0
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (req.signal?.aborted) throw err;
|
|
64
|
+
if (yieldedAny) throw err;
|
|
65
|
+
if (attempt >= maxRetries) throw err;
|
|
66
|
+
if (!isRetryable(err)) throw err;
|
|
67
|
+
const hint = readRetryAfter(err);
|
|
68
|
+
await sleep(hint !== null ? Math.min(hint, maxDelay) : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter), req.signal);
|
|
69
|
+
attempt++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read a `Retry-After` hint from a thrown error. Recognises:
|
|
74
|
+
*
|
|
75
|
+
* - `RateLimitExceededError`-shaped errors carrying a `retryAfterMs`
|
|
76
|
+
* field (already milliseconds).
|
|
77
|
+
* - HTTP-shaped errors carrying a `headers['retry-after']` value
|
|
78
|
+
* (numeric seconds; we convert to milliseconds).
|
|
79
|
+
* - HTTP-shaped errors carrying a `retryAfterSeconds` numeric field.
|
|
80
|
+
*
|
|
81
|
+
* Returns the resolved delay in milliseconds or `null` when no hint
|
|
82
|
+
* is available.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
function readRetryAfter(err) {
|
|
87
|
+
if (err === null || typeof err !== "object") return null;
|
|
88
|
+
const e = err;
|
|
89
|
+
if (typeof e.retryAfterMs === "number" && Number.isFinite(e.retryAfterMs) && e.retryAfterMs >= 0) return e.retryAfterMs;
|
|
90
|
+
if (typeof e.retryAfterSeconds === "number" && Number.isFinite(e.retryAfterSeconds) && e.retryAfterSeconds >= 0) return Math.round(e.retryAfterSeconds * 1e3);
|
|
91
|
+
const headerValue = e.headers?.["retry-after"] ?? e.headers?.["Retry-After"] ?? e.headers?.["RETRY-AFTER"];
|
|
92
|
+
if (typeof headerValue === "string" && headerValue.length > 0) {
|
|
93
|
+
const seconds = Number(headerValue);
|
|
94
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1e3);
|
|
95
|
+
const httpDateMs = Date.parse(headerValue);
|
|
96
|
+
if (Number.isFinite(httpDateMs)) {
|
|
97
|
+
const delta = httpDateMs - Date.now();
|
|
98
|
+
return delta > 0 ? delta : 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function defaultRetryable(err) {
|
|
104
|
+
if (err === null || typeof err !== "object") return false;
|
|
105
|
+
if (isAbortError(err)) return false;
|
|
106
|
+
const e = err;
|
|
107
|
+
if (e.kind === "transient" || e.kind === "rate-limit" || e.kind === "rate-limit-exceeded" || e.kind === "capacity") return true;
|
|
108
|
+
if (typeof e.status === "number" && e.status === 429) return true;
|
|
109
|
+
if (typeof e.status === "number" && e.status >= 500 && e.status < 600) return true;
|
|
110
|
+
if (typeof e.status === "number" && e.status === 0) return true;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
function computeDelay(backoff, initial, attempt, maxDelay, jitter) {
|
|
114
|
+
let delay;
|
|
115
|
+
switch (backoff) {
|
|
116
|
+
case "exponential":
|
|
117
|
+
delay = initial * 2 ** attempt;
|
|
118
|
+
break;
|
|
119
|
+
case "linear":
|
|
120
|
+
delay = initial * (attempt + 1);
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
delay = initial;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
delay = Math.min(delay, maxDelay);
|
|
127
|
+
if (jitter) {
|
|
128
|
+
const factor = .5 + Math.random() * .5;
|
|
129
|
+
delay = Math.floor(delay * factor);
|
|
130
|
+
}
|
|
131
|
+
return delay;
|
|
132
|
+
}
|
|
133
|
+
function defaultSleep(ms, signal) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
if (signal?.aborted) {
|
|
136
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const timer = setTimeout(() => {
|
|
140
|
+
cleanup();
|
|
141
|
+
resolve();
|
|
142
|
+
}, ms);
|
|
143
|
+
const onAbort = () => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
146
|
+
};
|
|
147
|
+
function cleanup() {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
signal?.removeEventListener("abort", onAbort);
|
|
150
|
+
}
|
|
151
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
export { withRetry };
|
|
157
|
+
//# sourceMappingURL=with-retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-retry.js","names":["delay: number"],"sources":["../../src/middleware/with-retry.ts"],"sourcesContent":["/**\n * `withRetry` — retry idempotent failures (5xx, network, timeouts)\n * with exponential backoff + jitter. Lives outside `withRateLimit`\n * per the canonical order so retried requests respect the rate-limit\n * bucket of the next attempt.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderEvent, ProviderRequest, ProviderResponse } from '@graphorin/core';\n\nimport { isAbortError } from '../internal/abort.js';\nimport { defineProviderMiddleware } from './compose.js';\n\n/**\n * Options for {@link withRetry}.\n *\n * @stable\n */\nexport interface WithRetryOptions {\n readonly maxRetries?: number;\n readonly backoff?: 'exponential' | 'linear' | 'constant';\n readonly initialDelayMs?: number;\n readonly maxDelayMs?: number;\n readonly jitter?: boolean;\n readonly retryableErrors?: (err: unknown) => boolean;\n /** Optional sleep override (test fixtures use a synchronous resolver). */\n readonly sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;\n}\n\n/**\n * @stable\n */\nexport const withRetry = defineProviderMiddleware<WithRetryOptions>({\n kind: 'withRetry',\n factory: (opts: WithRetryOptions) => {\n const maxRetries = opts.maxRetries ?? 3;\n const backoff = opts.backoff ?? 'exponential';\n const initialDelay = opts.initialDelayMs ?? 250;\n const maxDelay = opts.maxDelayMs ?? 30_000;\n const jitter = opts.jitter ?? true;\n const isRetryable = opts.retryableErrors ?? defaultRetryable;\n const sleep = opts.sleepImpl ?? defaultSleep;\n return (next: Provider): Provider => ({\n name: next.name,\n modelId: next.modelId,\n capabilities: next.capabilities,\n ...(next.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: next.acceptsSensitivity }\n : {}),\n stream(req) {\n return retryingStream(\n next,\n req,\n maxRetries,\n backoff,\n initialDelay,\n maxDelay,\n jitter,\n isRetryable,\n sleep,\n );\n },\n async generate(req) {\n let attempt = 0;\n for (;;) {\n try {\n return await next.generate(req);\n } catch (err) {\n if (req.signal?.aborted) throw err;\n if (attempt >= maxRetries) throw err;\n if (!isRetryable(err)) throw err;\n const hint = readRetryAfter(err);\n const delay =\n hint !== null\n ? Math.min(hint, maxDelay)\n : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter);\n await sleep(delay, req.signal);\n attempt++;\n }\n }\n },\n ...(next.countTokens ? { countTokens: next.countTokens.bind(next) } : {}),\n });\n },\n});\n\nasync function* retryingStream(\n next: Provider,\n req: ProviderRequest,\n maxRetries: number,\n backoff: 'exponential' | 'linear' | 'constant',\n initialDelay: number,\n maxDelay: number,\n jitter: boolean,\n isRetryable: (err: unknown) => boolean,\n sleep: (ms: number, signal?: AbortSignal) => Promise<void>,\n): AsyncIterable<ProviderEvent> {\n let attempt = 0;\n // Stream-level (not per-attempt): once *any* event has reached the\n // consumer, a retryable failure mid-stream must NOT restart the stream —\n // that would replay stream-start + already-delivered text/tool-call events\n // into the same iteration (duplicate output, potential double tool execution).\n // `withFallback` upholds the same invariant. Pre-yield failures still retry.\n let yieldedAny = false;\n for (;;) {\n try {\n for await (const event of next.stream(req)) {\n yieldedAny = true;\n yield event;\n }\n if (yieldedAny) return;\n // Empty stream — surface a finish so callers do not hang.\n yield {\n type: 'finish',\n finishReason: 'stop',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n };\n return;\n } catch (err) {\n if (req.signal?.aborted) throw err;\n // PS-1: never restart a stream that already emitted events.\n if (yieldedAny) throw err;\n if (attempt >= maxRetries) throw err;\n if (!isRetryable(err)) throw err;\n const hint = readRetryAfter(err);\n const delay =\n hint !== null\n ? Math.min(hint, maxDelay)\n : computeDelay(backoff, initialDelay, attempt, maxDelay, jitter);\n await sleep(delay, req.signal);\n attempt++;\n }\n }\n}\n\n/**\n * Read a `Retry-After` hint from a thrown error. Recognises:\n *\n * - `RateLimitExceededError`-shaped errors carrying a `retryAfterMs`\n * field (already milliseconds).\n * - HTTP-shaped errors carrying a `headers['retry-after']` value\n * (numeric seconds; we convert to milliseconds).\n * - HTTP-shaped errors carrying a `retryAfterSeconds` numeric field.\n *\n * Returns the resolved delay in milliseconds or `null` when no hint\n * is available.\n *\n * @internal\n */\nfunction readRetryAfter(err: unknown): number | null {\n if (err === null || typeof err !== 'object') return null;\n const e = err as {\n retryAfterMs?: number;\n retryAfterSeconds?: number;\n headers?: Record<string, string | undefined>;\n };\n if (\n typeof e.retryAfterMs === 'number' &&\n Number.isFinite(e.retryAfterMs) &&\n e.retryAfterMs >= 0\n ) {\n return e.retryAfterMs;\n }\n if (\n typeof e.retryAfterSeconds === 'number' &&\n Number.isFinite(e.retryAfterSeconds) &&\n e.retryAfterSeconds >= 0\n ) {\n return Math.round(e.retryAfterSeconds * 1000);\n }\n const headerValue =\n e.headers?.['retry-after'] ?? e.headers?.['Retry-After'] ?? e.headers?.['RETRY-AFTER'];\n if (typeof headerValue === 'string' && headerValue.length > 0) {\n const seconds = Number(headerValue);\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const httpDateMs = Date.parse(headerValue);\n if (Number.isFinite(httpDateMs)) {\n const delta = httpDateMs - Date.now();\n return delta > 0 ? delta : 0;\n }\n }\n return null;\n}\n\nfunction defaultRetryable(err: unknown): boolean {\n if (err === null || typeof err !== 'object') return false;\n // An aborted request is never retryable, even when it surfaces as a\n // `status: 0` network error (PS-2). The retry loop also short-circuits on\n // `req.signal?.aborted`, but the predicate must exclude abort independently\n // so an internally-aborted call is not retried.\n if (isAbortError(err)) return false;\n const e = err as { kind?: string; status?: number; name?: string };\n if (\n e.kind === 'transient' ||\n e.kind === 'rate-limit' ||\n e.kind === 'rate-limit-exceeded' ||\n e.kind === 'capacity'\n ) {\n return true;\n }\n if (typeof e.status === 'number' && e.status === 429) return true;\n if (typeof e.status === 'number' && e.status >= 500 && e.status < 600) return true;\n // PS-2: a `ProviderHttpError{ status: 0 }` is a fetch-level network failure\n // (ECONNREFUSED, DNS, connection reset) — exactly the transient class\n // `withRetry` documents. Retry it (abort already excluded above).\n if (typeof e.status === 'number' && e.status === 0) return true;\n return false;\n}\n\nfunction computeDelay(\n backoff: 'exponential' | 'linear' | 'constant',\n initial: number,\n attempt: number,\n maxDelay: number,\n jitter: boolean,\n): number {\n let delay: number;\n switch (backoff) {\n case 'exponential':\n delay = initial * 2 ** attempt;\n break;\n case 'linear':\n delay = initial * (attempt + 1);\n break;\n default:\n delay = initial;\n break;\n }\n delay = Math.min(delay, maxDelay);\n if (jitter) {\n const factor = 0.5 + Math.random() * 0.5; // 0.5..1.0\n delay = Math.floor(delay * factor);\n }\n return delay;\n}\n\nfunction defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason ?? new Error('aborted'));\n return;\n }\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, ms);\n const onAbort = () => {\n cleanup();\n reject(signal?.reason ?? new Error('aborted'));\n };\n function cleanup(): void {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n// Cast for return-type compatibility with the discriminated union in\n// `Provider.generate(...)` (TypeScript has no concept of \"exhaustive\n// retry loop\" so the empty exit path is unreachable).\nexport type _RetryProviderResponseGuard = ProviderResponse;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as _graphorin_core0 from "@graphorin/core";
|
|
2
|
+
import { Tracer } from "@graphorin/core";
|
|
3
|
+
|
|
4
|
+
//#region src/middleware/with-tracing.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link withTracing}.
|
|
8
|
+
*
|
|
9
|
+
* @stable
|
|
10
|
+
*/
|
|
11
|
+
interface WithTracingOptions {
|
|
12
|
+
/** Tracer instance. Defaults to a no-op tracer if unset. */
|
|
13
|
+
readonly tracer?: Tracer;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @stable
|
|
17
|
+
*/
|
|
18
|
+
declare const withTracing: (opts: WithTracingOptions) => _graphorin_core0.ProviderMiddleware;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { WithTracingOptions, withTracing };
|
|
21
|
+
//# sourceMappingURL=with-tracing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-tracing.d.ts","names":[],"sources":["../../src/middleware/with-tracing.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UAuBiB,kBAAA;;oBAEG;;;;;cAMP,oBAAW,uBAAA,gBAAA,CAAA"}
|