@rheonic/sdk 0.1.0-beta.6 → 0.1.0-beta.8
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 +8 -2
- package/README.md +35 -3
- package/dist/client.d.ts +1 -0
- package/dist/client.js +4 -0
- package/dist/protectEngine.d.ts +18 -1
- package/dist/protectEngine.js +62 -19
- package/dist/providers/anthropicAdapter.js +3 -3
- package/dist/providers/googleAdapter.js +3 -3
- package/dist/providers/openaiAdapter.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
3
|
+
All notable changes to `@rheonic/sdk` will be documented in this file.
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
7
|
- Publish-ready changelog entries will be added here for the next release.
|
|
8
8
|
|
|
9
|
-
## 0.1.0
|
|
9
|
+
## 0.1.0-beta.7
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `RHEONICBlockedError` now exposes structured block feedback for apps and agents: `reason`, `retry_after_seconds`, `blocked_until`, `trace_id`, `request_id`, and `snapshot`.
|
|
13
|
+
- Fail-closed protect fallback now reports `reason="fail_closed"` instead of the generic `decision_unavailable` on blocked requests.
|
|
14
|
+
|
|
15
|
+
## 0.1.0-beta.6
|
|
10
16
|
|
|
11
17
|
### Added
|
|
12
18
|
- Initial public beta release of the Rheonic Node SDK.
|
package/README.md
CHANGED
|
@@ -48,7 +48,19 @@ try {
|
|
|
48
48
|
});
|
|
49
49
|
} catch (error) {
|
|
50
50
|
if (error instanceof RHEONICBlockedError) {
|
|
51
|
-
console.log(
|
|
51
|
+
console.log(
|
|
52
|
+
JSON.stringify(
|
|
53
|
+
{
|
|
54
|
+
reason: error.reason,
|
|
55
|
+
retry_after_seconds: error.retry_after_seconds,
|
|
56
|
+
blocked_until: error.blocked_until,
|
|
57
|
+
trace_id: error.trace_id,
|
|
58
|
+
request_id: error.request_id,
|
|
59
|
+
},
|
|
60
|
+
null,
|
|
61
|
+
2,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
```
|
|
@@ -74,7 +86,13 @@ try {
|
|
|
74
86
|
});
|
|
75
87
|
} catch (error) {
|
|
76
88
|
if (error instanceof RHEONICBlockedError) {
|
|
77
|
-
console.log(
|
|
89
|
+
console.log(JSON.stringify({
|
|
90
|
+
reason: error.reason,
|
|
91
|
+
retry_after_seconds: error.retry_after_seconds,
|
|
92
|
+
blocked_until: error.blocked_until,
|
|
93
|
+
trace_id: error.trace_id,
|
|
94
|
+
request_id: error.request_id,
|
|
95
|
+
}, null, 2));
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
```
|
|
@@ -97,11 +115,25 @@ try {
|
|
|
97
115
|
await model.generateContent("hello");
|
|
98
116
|
} catch (error) {
|
|
99
117
|
if (error instanceof RHEONICBlockedError) {
|
|
100
|
-
console.log(
|
|
118
|
+
console.log(JSON.stringify({
|
|
119
|
+
reason: error.reason,
|
|
120
|
+
retry_after_seconds: error.retry_after_seconds,
|
|
121
|
+
blocked_until: error.blocked_until,
|
|
122
|
+
trace_id: error.trace_id,
|
|
123
|
+
request_id: error.request_id,
|
|
124
|
+
}, null, 2));
|
|
101
125
|
}
|
|
102
126
|
}
|
|
103
127
|
```
|
|
104
128
|
|
|
129
|
+
`RHEONICBlockedError.reason` is meant to be operator-relevant. The main values are:
|
|
130
|
+
- `tok_cap_breach`
|
|
131
|
+
- `req_cap_breach`
|
|
132
|
+
- `cooldown_active`
|
|
133
|
+
- `fail_closed`
|
|
134
|
+
|
|
135
|
+
If Protect is `fail_open`, timeout or availability problems stay internal and the provider call continues. If Protect is `fail_closed`, the SDK raises `RHEONICBlockedError` with the feedback fields shown above.
|
|
136
|
+
|
|
105
137
|
Keep one long-lived SDK client per app process. Initialize it during app startup and reuse it for all capture and instrumentation calls so Rheonic can avoid repeated protect cold-start latency.
|
|
106
138
|
|
|
107
139
|
## Optional: custom event capture
|
package/dist/client.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export declare class Client {
|
|
|
42
42
|
private warmupCompleted;
|
|
43
43
|
constructor(config: ClientConfig);
|
|
44
44
|
captureEvent(event: EventPayload): Promise<void>;
|
|
45
|
+
captureEventAndFlush(event: EventPayload): Promise<void>;
|
|
45
46
|
getStats(): ClientStats;
|
|
46
47
|
flush(): Promise<void>;
|
|
47
48
|
evaluateProtectDecision(context: ProtectContext): Promise<ProtectEvaluation>;
|
package/dist/client.js
CHANGED
package/dist/protectEngine.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export interface ProtectContext {
|
|
|
10
10
|
export interface ProtectEvaluation {
|
|
11
11
|
decision: ProtectDecision;
|
|
12
12
|
reason: string;
|
|
13
|
+
trace_id: string;
|
|
14
|
+
request_id: string;
|
|
15
|
+
blocked_until?: string;
|
|
16
|
+
retry_after_seconds?: number;
|
|
13
17
|
snapshot?: Record<string, unknown>;
|
|
14
18
|
applyClampEnabled?: boolean;
|
|
15
19
|
clamp?: {
|
|
@@ -19,7 +23,19 @@ export interface ProtectEvaluation {
|
|
|
19
23
|
}
|
|
20
24
|
export declare class RHEONICBlockedError extends Error {
|
|
21
25
|
readonly reason: string;
|
|
22
|
-
|
|
26
|
+
readonly trace_id: string;
|
|
27
|
+
readonly request_id: string;
|
|
28
|
+
readonly blocked_until?: string;
|
|
29
|
+
readonly retry_after_seconds?: number;
|
|
30
|
+
readonly snapshot?: Record<string, unknown>;
|
|
31
|
+
constructor(params: {
|
|
32
|
+
reason: string;
|
|
33
|
+
trace_id: string;
|
|
34
|
+
request_id: string;
|
|
35
|
+
blocked_until?: string;
|
|
36
|
+
retry_after_seconds?: number;
|
|
37
|
+
snapshot?: Record<string, unknown>;
|
|
38
|
+
});
|
|
23
39
|
}
|
|
24
40
|
export declare class ProtectEngine {
|
|
25
41
|
private readonly baseUrl;
|
|
@@ -41,6 +57,7 @@ export declare class ProtectEngine {
|
|
|
41
57
|
debugLog?: (message: string, meta?: Record<string, unknown>) => void;
|
|
42
58
|
});
|
|
43
59
|
evaluate(context: ProtectContext): Promise<ProtectEvaluation>;
|
|
60
|
+
private fallbackEvaluation;
|
|
44
61
|
bootstrap(): Promise<void>;
|
|
45
62
|
private reportDecisionTimeout;
|
|
46
63
|
private reportDecisionUnavailable;
|
package/dist/protectEngine.js
CHANGED
|
@@ -4,10 +4,20 @@ import { requestJson } from "./httpTransport.js";
|
|
|
4
4
|
import { bindTraceContext, generateSpanId, generateTraceId, getTraceId } from "./logger.js";
|
|
5
5
|
export class RHEONICBlockedError extends Error {
|
|
6
6
|
reason;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
trace_id;
|
|
8
|
+
request_id;
|
|
9
|
+
blocked_until;
|
|
10
|
+
retry_after_seconds;
|
|
11
|
+
snapshot;
|
|
12
|
+
constructor(params) {
|
|
13
|
+
super(`Request blocked by Rheonic: ${params.reason}`);
|
|
9
14
|
this.name = "RHEONICBlockedError";
|
|
10
|
-
this.reason = reason;
|
|
15
|
+
this.reason = params.reason;
|
|
16
|
+
this.trace_id = params.trace_id;
|
|
17
|
+
this.request_id = params.request_id;
|
|
18
|
+
this.blocked_until = params.blocked_until;
|
|
19
|
+
this.retry_after_seconds = params.retry_after_seconds;
|
|
20
|
+
this.snapshot = params.snapshot;
|
|
11
21
|
}
|
|
12
22
|
}
|
|
13
23
|
export class ProtectEngine {
|
|
@@ -35,6 +45,8 @@ export class ProtectEngine {
|
|
|
35
45
|
this.cooldownReason = null;
|
|
36
46
|
}
|
|
37
47
|
async evaluate(context) {
|
|
48
|
+
const requestId = randomUUID();
|
|
49
|
+
const traceId = generateTraceId();
|
|
38
50
|
const nowMs = Date.now();
|
|
39
51
|
if (this.cooldownUntilMs !== null && nowMs < this.cooldownUntilMs) {
|
|
40
52
|
this.debugLog?.("Protect preflight blocked locally from cached cooldown", {
|
|
@@ -42,15 +54,20 @@ export class ProtectEngine {
|
|
|
42
54
|
decision: "block",
|
|
43
55
|
reason: this.cooldownReason ?? "cooldown_active",
|
|
44
56
|
});
|
|
45
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
decision: "block",
|
|
59
|
+
reason: this.cooldownReason ?? "cooldown_active",
|
|
60
|
+
trace_id: traceId,
|
|
61
|
+
request_id: requestId,
|
|
62
|
+
blocked_until: formatBlockedUntilMs(this.cooldownUntilMs),
|
|
63
|
+
retry_after_seconds: toRetryAfterSeconds(this.cooldownUntilMs, nowMs),
|
|
64
|
+
};
|
|
46
65
|
}
|
|
47
66
|
const controller = new AbortController();
|
|
48
67
|
const timeoutMs = this.decisionTimeoutMs > 0 ? this.decisionTimeoutMs : this.fallbackRequestTimeoutMs;
|
|
49
68
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
50
69
|
timeout.unref?.();
|
|
51
70
|
const startedAt = Date.now();
|
|
52
|
-
const requestId = randomUUID();
|
|
53
|
-
const traceId = generateTraceId();
|
|
54
71
|
const spanId = generateSpanId();
|
|
55
72
|
try {
|
|
56
73
|
const response = await bindTraceContext(traceId, spanId, async () => await requestJson(`${this.baseUrl}/api/v1/protect/decision`, {
|
|
@@ -72,10 +89,8 @@ export class ProtectEngine {
|
|
|
72
89
|
status_code: response.status,
|
|
73
90
|
latency_ms: Date.now() - startedAt,
|
|
74
91
|
});
|
|
75
|
-
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId);
|
|
76
|
-
return this.
|
|
77
|
-
? { decision: "block", reason: "decision_unavailable" }
|
|
78
|
-
: { decision: "allow", reason: "decision_unavailable" };
|
|
92
|
+
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
|
|
93
|
+
return this.fallbackEvaluation(traceId, requestId);
|
|
79
94
|
}
|
|
80
95
|
const parsed = (await response.json());
|
|
81
96
|
const decision = parseDecision(parsed.decision);
|
|
@@ -107,6 +122,10 @@ export class ProtectEngine {
|
|
|
107
122
|
return {
|
|
108
123
|
decision,
|
|
109
124
|
reason,
|
|
125
|
+
trace_id: traceId,
|
|
126
|
+
request_id: requestId,
|
|
127
|
+
blocked_until: typeof parsed.blocked_until === "string" ? parsed.blocked_until : undefined,
|
|
128
|
+
retry_after_seconds: parseRetryAfterSeconds(parsed.retry_after_seconds),
|
|
110
129
|
snapshot: parseSnapshot(parsed.snapshot),
|
|
111
130
|
applyClampEnabled: typeof parsed.apply_clamp_enabled === "boolean" ? parsed.apply_clamp_enabled : undefined,
|
|
112
131
|
clamp: parseClamp(parsed.clamp),
|
|
@@ -120,7 +139,7 @@ export class ProtectEngine {
|
|
|
120
139
|
latency_ms: Date.now() - startedAt,
|
|
121
140
|
timeout_ms: timeoutMs,
|
|
122
141
|
});
|
|
123
|
-
void this.reportDecisionTimeout(context.provider, typeof context.model === "string" ? context.model : undefined, requestId);
|
|
142
|
+
void this.reportDecisionTimeout(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
|
|
124
143
|
}
|
|
125
144
|
else {
|
|
126
145
|
this.debugLog?.("Protect preflight failed", {
|
|
@@ -128,13 +147,19 @@ export class ProtectEngine {
|
|
|
128
147
|
latency_ms: Date.now() - startedAt,
|
|
129
148
|
error_type: extractErrorType(error),
|
|
130
149
|
});
|
|
131
|
-
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId);
|
|
150
|
+
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
|
|
132
151
|
}
|
|
133
|
-
return this.
|
|
134
|
-
? { decision: "block", reason: "decision_unavailable" }
|
|
135
|
-
: { decision: "allow", reason: "decision_unavailable" };
|
|
152
|
+
return this.fallbackEvaluation(traceId, requestId);
|
|
136
153
|
}
|
|
137
154
|
}
|
|
155
|
+
fallbackEvaluation(traceId, requestId) {
|
|
156
|
+
return {
|
|
157
|
+
decision: this.failMode === "closed" ? "block" : "allow",
|
|
158
|
+
reason: this.failMode === "closed" ? "fail_closed" : "decision_unavailable",
|
|
159
|
+
trace_id: traceId,
|
|
160
|
+
request_id: requestId,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
138
163
|
async bootstrap() {
|
|
139
164
|
try {
|
|
140
165
|
const response = await requestJson(`${this.baseUrl}/api/v1/protect/config`, {
|
|
@@ -162,14 +187,14 @@ export class ProtectEngine {
|
|
|
162
187
|
// Best effort only; keep local defaults if bootstrap fails.
|
|
163
188
|
}
|
|
164
189
|
}
|
|
165
|
-
async reportDecisionTimeout(provider, model, requestId) {
|
|
190
|
+
async reportDecisionTimeout(provider, model, requestId, traceId) {
|
|
166
191
|
try {
|
|
167
192
|
await requestJson(`${this.baseUrl}/api/v1/protect/decision-timeout`, {
|
|
168
193
|
method: "POST",
|
|
169
194
|
headers: {
|
|
170
195
|
"Content-Type": "application/json",
|
|
171
196
|
"X-Project-Ingest-Key": this.ingestKey,
|
|
172
|
-
"X-Trace-ID":
|
|
197
|
+
"X-Trace-ID": traceId,
|
|
173
198
|
"X-Span-ID": generateSpanId(),
|
|
174
199
|
"X-Rheonic-Protect-Request-Id": requestId,
|
|
175
200
|
},
|
|
@@ -180,14 +205,14 @@ export class ProtectEngine {
|
|
|
180
205
|
// Swallow timeout reporting errors; protect evaluation must never throw here.
|
|
181
206
|
}
|
|
182
207
|
}
|
|
183
|
-
async reportDecisionUnavailable(provider, model, requestId) {
|
|
208
|
+
async reportDecisionUnavailable(provider, model, requestId, traceId) {
|
|
184
209
|
try {
|
|
185
210
|
await requestJson(`${this.baseUrl}/api/v1/protect/decision-unavailable`, {
|
|
186
211
|
method: "POST",
|
|
187
212
|
headers: {
|
|
188
213
|
"Content-Type": "application/json",
|
|
189
214
|
"X-Project-Ingest-Key": this.ingestKey,
|
|
190
|
-
"X-Trace-ID":
|
|
215
|
+
"X-Trace-ID": traceId,
|
|
191
216
|
"X-Span-ID": generateSpanId(),
|
|
192
217
|
"X-Rheonic-Protect-Request-Id": requestId,
|
|
193
218
|
},
|
|
@@ -234,6 +259,24 @@ function parseDecision(value) {
|
|
|
234
259
|
}
|
|
235
260
|
return "allow";
|
|
236
261
|
}
|
|
262
|
+
function parseRetryAfterSeconds(value) {
|
|
263
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
264
|
+
return Math.floor(value);
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
function formatBlockedUntilMs(value) {
|
|
269
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
return new Date(value).toISOString();
|
|
273
|
+
}
|
|
274
|
+
function toRetryAfterSeconds(blockedUntilMs, nowMs) {
|
|
275
|
+
if (typeof blockedUntilMs !== "number" || !Number.isFinite(blockedUntilMs) || blockedUntilMs <= nowMs) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
return Math.max(0, Math.ceil((blockedUntilMs - nowMs) / 1000));
|
|
279
|
+
}
|
|
237
280
|
function parseFailMode(value) {
|
|
238
281
|
if (value === "open" || value === "closed") {
|
|
239
282
|
return value;
|
|
@@ -42,13 +42,13 @@ export function instrumentAnthropic(anthropicClient, options) {
|
|
|
42
42
|
}
|
|
43
43
|
const protectDecision = await options.client.evaluateProtectDecision(protectPayload);
|
|
44
44
|
if (protectDecision.decision === "block") {
|
|
45
|
-
throw new RHEONICBlockedError(protectDecision
|
|
45
|
+
throw new RHEONICBlockedError(protectDecision);
|
|
46
46
|
}
|
|
47
47
|
const callArgs = maybeApplyAnthropicClamp(args, protectDecision);
|
|
48
48
|
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
49
49
|
try {
|
|
50
50
|
const response = await originalCreate(...callArgs);
|
|
51
|
-
|
|
51
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
52
52
|
provider: "anthropic",
|
|
53
53
|
model: extractResponseModel(response) ?? requestedModel,
|
|
54
54
|
environment: options.environment ?? options.client.environment,
|
|
@@ -69,7 +69,7 @@ export function instrumentAnthropic(anthropicClient, options) {
|
|
|
69
69
|
return response;
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
|
|
72
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
73
73
|
provider: "anthropic",
|
|
74
74
|
model: requestedModel,
|
|
75
75
|
environment: options.environment ?? options.client.environment,
|
|
@@ -42,13 +42,13 @@ export function instrumentGoogle(googleModel, options) {
|
|
|
42
42
|
}
|
|
43
43
|
const protectDecision = await options.client.evaluateProtectDecision(protectPayload);
|
|
44
44
|
if (protectDecision.decision === "block") {
|
|
45
|
-
throw new RHEONICBlockedError(protectDecision
|
|
45
|
+
throw new RHEONICBlockedError(protectDecision);
|
|
46
46
|
}
|
|
47
47
|
const callArgs = maybeApplyGoogleClamp(args, protectDecision);
|
|
48
48
|
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
49
49
|
try {
|
|
50
50
|
const response = await originalGenerate(...callArgs);
|
|
51
|
-
|
|
51
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
52
52
|
provider: "google",
|
|
53
53
|
model: requestedModel,
|
|
54
54
|
environment: options.environment ?? options.client.environment,
|
|
@@ -69,7 +69,7 @@ export function instrumentGoogle(googleModel, options) {
|
|
|
69
69
|
return response;
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
|
|
72
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
73
73
|
provider: "google",
|
|
74
74
|
model: requestedModel,
|
|
75
75
|
environment: options.environment ?? options.client.environment,
|
|
@@ -43,13 +43,13 @@ export function instrumentOpenAI(openaiClient, options) {
|
|
|
43
43
|
...protectPayload,
|
|
44
44
|
});
|
|
45
45
|
if (protectDecision.decision === "block") {
|
|
46
|
-
throw new RHEONICBlockedError(protectDecision
|
|
46
|
+
throw new RHEONICBlockedError(protectDecision);
|
|
47
47
|
}
|
|
48
48
|
const callArgs = maybeApplyOpenAIClamp(args, protectDecision);
|
|
49
49
|
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
50
50
|
try {
|
|
51
51
|
const response = await originalCreate(...callArgs);
|
|
52
|
-
|
|
52
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
53
53
|
provider: "openai",
|
|
54
54
|
model: extractResponseModel(response) ?? model,
|
|
55
55
|
environment: options.environment ?? options.client.environment,
|
|
@@ -69,7 +69,7 @@ export function instrumentOpenAI(openaiClient, options) {
|
|
|
69
69
|
return response;
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
|
|
72
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
73
73
|
provider: "openai",
|
|
74
74
|
model,
|
|
75
75
|
environment: options.environment ?? options.client.environment,
|
package/package.json
CHANGED