@rheonic/sdk 0.1.0-beta.1
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 +21 -0
- package/README.md +150 -0
- package/dist/client.d.ts +59 -0
- package/dist/client.js +305 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +15 -0
- package/dist/costCalculator.d.ts +3 -0
- package/dist/costCalculator.js +6 -0
- package/dist/eventBuilder.d.ts +35 -0
- package/dist/eventBuilder.js +15 -0
- package/dist/httpTransport.d.ts +12 -0
- package/dist/httpTransport.js +100 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +60 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +56 -0
- package/dist/protectEngine.d.ts +48 -0
- package/dist/protectEngine.js +255 -0
- package/dist/providerModelValidation.d.ts +7 -0
- package/dist/providerModelValidation.js +26 -0
- package/dist/providers/anthropicAdapter.d.ts +9 -0
- package/dist/providers/anthropicAdapter.js +189 -0
- package/dist/providers/googleAdapter.d.ts +9 -0
- package/dist/providers/googleAdapter.js +212 -0
- package/dist/providers/openaiAdapter.d.ts +9 -0
- package/dist/providers/openaiAdapter.js +203 -0
- package/dist/rateLimiter.d.ts +3 -0
- package/dist/rateLimiter.js +6 -0
- package/dist/tokenEstimator.d.ts +2 -0
- package/dist/tokenEstimator.js +64 -0
- package/package.json +74 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
const HTTP_AGENT = new http.Agent({
|
|
4
|
+
keepAlive: true,
|
|
5
|
+
maxSockets: 32,
|
|
6
|
+
});
|
|
7
|
+
const HTTPS_AGENT = new https.Agent({
|
|
8
|
+
keepAlive: true,
|
|
9
|
+
maxSockets: 32,
|
|
10
|
+
});
|
|
11
|
+
class BufferedJsonResponse {
|
|
12
|
+
status;
|
|
13
|
+
payload;
|
|
14
|
+
ok;
|
|
15
|
+
constructor(status, payload) {
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.payload = payload;
|
|
18
|
+
this.ok = status >= 200 && status < 300;
|
|
19
|
+
}
|
|
20
|
+
async json() {
|
|
21
|
+
if (!this.payload) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(this.payload);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function requestJson(url, options) {
|
|
28
|
+
const mockedFetch = getMockedFetchOverride();
|
|
29
|
+
if (mockedFetch) {
|
|
30
|
+
const response = await mockedFetch(url, {
|
|
31
|
+
method: options.method,
|
|
32
|
+
headers: options.headers,
|
|
33
|
+
body: options.body,
|
|
34
|
+
signal: options.signal,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
ok: response.ok,
|
|
38
|
+
status: response.status,
|
|
39
|
+
json: async () => await response.json(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const target = new URL(url);
|
|
43
|
+
const useHttps = target.protocol === "https:";
|
|
44
|
+
const transport = useHttps ? https : http;
|
|
45
|
+
const agent = useHttps ? HTTPS_AGENT : HTTP_AGENT;
|
|
46
|
+
return await new Promise((resolve, reject) => {
|
|
47
|
+
const req = transport.request({
|
|
48
|
+
protocol: target.protocol,
|
|
49
|
+
hostname: target.hostname,
|
|
50
|
+
port: target.port || (useHttps ? 443 : 80),
|
|
51
|
+
path: `${target.pathname}${target.search}`,
|
|
52
|
+
method: options.method,
|
|
53
|
+
headers: options.headers,
|
|
54
|
+
agent,
|
|
55
|
+
}, (res) => {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
res.on("data", (chunk) => {
|
|
58
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
59
|
+
});
|
|
60
|
+
res.on("end", () => {
|
|
61
|
+
resolve(new BufferedJsonResponse(res.statusCode ?? 0, Buffer.concat(chunks).toString("utf-8")));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
req.on("error", reject);
|
|
65
|
+
let abortHandler;
|
|
66
|
+
if (options.signal) {
|
|
67
|
+
if (options.signal.aborted) {
|
|
68
|
+
req.destroy(createAbortError());
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
abortHandler = () => req.destroy(createAbortError());
|
|
72
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
req.on("close", () => {
|
|
76
|
+
if (abortHandler && options.signal) {
|
|
77
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (options.body) {
|
|
81
|
+
req.write(options.body);
|
|
82
|
+
}
|
|
83
|
+
req.end();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function createAbortError() {
|
|
87
|
+
const error = new Error("Request aborted");
|
|
88
|
+
error.name = "AbortError";
|
|
89
|
+
return error;
|
|
90
|
+
}
|
|
91
|
+
function getMockedFetchOverride() {
|
|
92
|
+
if (typeof globalThis.fetch !== "function") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const source = Function.prototype.toString.call(globalThis.fetch);
|
|
96
|
+
if (source.includes("[native code]")) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return globalThis.fetch.bind(globalThis);
|
|
100
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Client, type ClientConfig, type ClientStats, type OverflowPolicy } from "./client.js";
|
|
2
|
+
import { buildEvent, type BuildEventInput, type EventPayload } from "./eventBuilder.js";
|
|
3
|
+
import { RHEONICBlockedError } from "./protectEngine.js";
|
|
4
|
+
import { RHEONICValidationError } from "./providerModelValidation.js";
|
|
5
|
+
import { type AnthropicInstrumentationOptions } from "./providers/anthropicAdapter.js";
|
|
6
|
+
import { type GoogleInstrumentationOptions } from "./providers/googleAdapter.js";
|
|
7
|
+
import { type OpenAIInstrumentationOptions } from "./providers/openaiAdapter.js";
|
|
8
|
+
export { Client, RHEONICBlockedError, RHEONICValidationError, type ClientConfig, type ClientStats, type OverflowPolicy, buildEvent, type BuildEventInput, type EventPayload, };
|
|
9
|
+
export declare function createClient(config: ClientConfig): Client;
|
|
10
|
+
export declare function captureEvent(event: EventPayload | BuildEventInput): Promise<void>;
|
|
11
|
+
export declare function instrumentOpenAI<T extends Record<string, any>>(openaiClient: T, options?: Omit<OpenAIInstrumentationOptions, "client"> & {
|
|
12
|
+
client?: Client;
|
|
13
|
+
}): T;
|
|
14
|
+
export declare function instrumentAnthropic<T extends Record<string, any>>(anthropicClient: T, options?: Omit<AnthropicInstrumentationOptions, "client"> & {
|
|
15
|
+
client?: Client;
|
|
16
|
+
}): T;
|
|
17
|
+
export declare function instrumentGoogle<T extends Record<string, any>>(googleModel: T, options?: Omit<GoogleInstrumentationOptions, "client"> & {
|
|
18
|
+
client?: Client;
|
|
19
|
+
}): T;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Client } from "./client.js";
|
|
2
|
+
import { buildEvent } from "./eventBuilder.js";
|
|
3
|
+
import { RHEONICBlockedError } from "./protectEngine.js";
|
|
4
|
+
import { RHEONICValidationError } from "./providerModelValidation.js";
|
|
5
|
+
import { instrumentAnthropic as instrumentAnthropicProvider } from "./providers/anthropicAdapter.js";
|
|
6
|
+
import { instrumentGoogle as instrumentGoogleProvider } from "./providers/googleAdapter.js";
|
|
7
|
+
import { instrumentOpenAI as instrumentOpenAIProvider } from "./providers/openaiAdapter.js";
|
|
8
|
+
let defaultClient = null;
|
|
9
|
+
export { Client, RHEONICBlockedError, RHEONICValidationError, buildEvent, };
|
|
10
|
+
export function createClient(config) {
|
|
11
|
+
if (defaultClient) {
|
|
12
|
+
defaultClient.close();
|
|
13
|
+
}
|
|
14
|
+
const client = new Client(config);
|
|
15
|
+
defaultClient = client;
|
|
16
|
+
return client;
|
|
17
|
+
}
|
|
18
|
+
export async function captureEvent(event) {
|
|
19
|
+
if (!defaultClient) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const payload = "provider" in event && "ts" in event ? event : buildEvent(event);
|
|
23
|
+
await defaultClient.captureEvent(payload);
|
|
24
|
+
}
|
|
25
|
+
export function instrumentOpenAI(openaiClient, options) {
|
|
26
|
+
const resolvedClient = options?.client ?? defaultClient;
|
|
27
|
+
if (!resolvedClient) {
|
|
28
|
+
return openaiClient;
|
|
29
|
+
}
|
|
30
|
+
return instrumentOpenAIProvider(openaiClient, {
|
|
31
|
+
client: resolvedClient,
|
|
32
|
+
environment: options?.environment,
|
|
33
|
+
endpoint: options?.endpoint,
|
|
34
|
+
feature: options?.feature,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function instrumentAnthropic(anthropicClient, options) {
|
|
38
|
+
const resolvedClient = options?.client ?? defaultClient;
|
|
39
|
+
if (!resolvedClient) {
|
|
40
|
+
return anthropicClient;
|
|
41
|
+
}
|
|
42
|
+
return instrumentAnthropicProvider(anthropicClient, {
|
|
43
|
+
client: resolvedClient,
|
|
44
|
+
environment: options?.environment,
|
|
45
|
+
endpoint: options?.endpoint,
|
|
46
|
+
feature: options?.feature,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function instrumentGoogle(googleModel, options) {
|
|
50
|
+
const resolvedClient = options?.client ?? defaultClient;
|
|
51
|
+
if (!resolvedClient) {
|
|
52
|
+
return googleModel;
|
|
53
|
+
}
|
|
54
|
+
return instrumentGoogleProvider(googleModel, {
|
|
55
|
+
client: resolvedClient,
|
|
56
|
+
environment: options?.environment,
|
|
57
|
+
endpoint: options?.endpoint,
|
|
58
|
+
feature: options?.feature,
|
|
59
|
+
});
|
|
60
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export declare function generateTraceId(): string;
|
|
3
|
+
export declare function generateSpanId(): string;
|
|
4
|
+
export declare function bindTraceContext<T>(traceId: string, spanId: string, fn: () => T): T;
|
|
5
|
+
export declare function getTraceId(): string;
|
|
6
|
+
export declare function getSpanId(): string;
|
|
7
|
+
export declare function emitLog(params: {
|
|
8
|
+
level: LogLevel;
|
|
9
|
+
event: string;
|
|
10
|
+
message: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
traceId?: string;
|
|
13
|
+
spanId?: string;
|
|
14
|
+
environment?: string;
|
|
15
|
+
}): void;
|
|
16
|
+
export {};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
const contextStorage = new AsyncLocalStorage();
|
|
4
|
+
const SENSITIVE_MARKERS = ["api_key", "apikey", "authorization", "cookie", "password", "secret", "token"];
|
|
5
|
+
const SERVICE_NAME = "sdk-node";
|
|
6
|
+
export function generateTraceId() {
|
|
7
|
+
return randomUUID();
|
|
8
|
+
}
|
|
9
|
+
export function generateSpanId() {
|
|
10
|
+
return randomUUID().replace(/-/g, "").slice(0, 16);
|
|
11
|
+
}
|
|
12
|
+
export function bindTraceContext(traceId, spanId, fn) {
|
|
13
|
+
return contextStorage.run({ traceId, spanId }, fn);
|
|
14
|
+
}
|
|
15
|
+
export function getTraceId() {
|
|
16
|
+
return contextStorage.getStore()?.traceId ?? "";
|
|
17
|
+
}
|
|
18
|
+
export function getSpanId() {
|
|
19
|
+
return contextStorage.getStore()?.spanId ?? "";
|
|
20
|
+
}
|
|
21
|
+
export function emitLog(params) {
|
|
22
|
+
const payload = {
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
level: params.level,
|
|
25
|
+
service: SERVICE_NAME,
|
|
26
|
+
env: (params.environment ?? process.env.RHEONIC_ENV ?? process.env.NODE_ENV ?? "dev").toLowerCase(),
|
|
27
|
+
trace_id: params.traceId ?? getTraceId(),
|
|
28
|
+
span_id: params.spanId ?? getSpanId(),
|
|
29
|
+
event: sanitizeEvent(params.event),
|
|
30
|
+
message: params.message,
|
|
31
|
+
metadata: sanitizeMetadata(params.metadata ?? {}),
|
|
32
|
+
};
|
|
33
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
34
|
+
}
|
|
35
|
+
function sanitizeEvent(value) {
|
|
36
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
37
|
+
return normalized || "log";
|
|
38
|
+
}
|
|
39
|
+
function sanitizeMetadata(value) {
|
|
40
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeValue(entry, key)]));
|
|
41
|
+
}
|
|
42
|
+
function sanitizeValue(value, key) {
|
|
43
|
+
if (key && SENSITIVE_MARKERS.some((marker) => key.toLowerCase().includes(marker))) {
|
|
44
|
+
return "[REDACTED]";
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.map((item) => sanitizeValue(item));
|
|
48
|
+
}
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [
|
|
51
|
+
childKey,
|
|
52
|
+
sanitizeValue(childValue, childKey),
|
|
53
|
+
]));
|
|
54
|
+
}
|
|
55
|
+
return value ?? null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type ProtectDecision = "allow" | "warn" | "block";
|
|
2
|
+
export type ProtectFailMode = "open" | "closed";
|
|
3
|
+
export interface ProtectContext {
|
|
4
|
+
provider: string;
|
|
5
|
+
model?: string | null;
|
|
6
|
+
feature?: string;
|
|
7
|
+
max_output_tokens?: number;
|
|
8
|
+
input_tokens_estimate?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ProtectEvaluation {
|
|
11
|
+
decision: ProtectDecision;
|
|
12
|
+
reason: string;
|
|
13
|
+
snapshot?: Record<string, unknown>;
|
|
14
|
+
applyClampEnabled?: boolean;
|
|
15
|
+
clamp?: {
|
|
16
|
+
recommended_max_output_tokens: number;
|
|
17
|
+
applied: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare class RHEONICBlockedError extends Error {
|
|
21
|
+
readonly reason: string;
|
|
22
|
+
constructor(reason: string);
|
|
23
|
+
}
|
|
24
|
+
export declare class ProtectEngine {
|
|
25
|
+
private readonly baseUrl;
|
|
26
|
+
private readonly ingestKey;
|
|
27
|
+
private readonly environment;
|
|
28
|
+
private readonly fallbackRequestTimeoutMs;
|
|
29
|
+
private readonly debugLog?;
|
|
30
|
+
private failMode;
|
|
31
|
+
private decisionTimeoutMs;
|
|
32
|
+
private cooldownUntilMs;
|
|
33
|
+
private cooldownReason;
|
|
34
|
+
constructor(params: {
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
ingestKey: string;
|
|
37
|
+
environment: string;
|
|
38
|
+
fallbackRequestTimeoutMs: number;
|
|
39
|
+
initialFailMode: ProtectFailMode;
|
|
40
|
+
initialDecisionTimeoutMs?: number;
|
|
41
|
+
debugLog?: (message: string, meta?: Record<string, unknown>) => void;
|
|
42
|
+
});
|
|
43
|
+
evaluate(context: ProtectContext): Promise<ProtectEvaluation>;
|
|
44
|
+
bootstrap(): Promise<void>;
|
|
45
|
+
private reportDecisionTimeout;
|
|
46
|
+
private reportDecisionUnavailable;
|
|
47
|
+
}
|
|
48
|
+
export declare const defaultProtectTimeoutMs: 150;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { sdkNodeConfig } from "./config.js";
|
|
3
|
+
import { requestJson } from "./httpTransport.js";
|
|
4
|
+
import { bindTraceContext, generateSpanId, generateTraceId, getTraceId } from "./logger.js";
|
|
5
|
+
export class RHEONICBlockedError extends Error {
|
|
6
|
+
reason;
|
|
7
|
+
constructor(reason) {
|
|
8
|
+
super(`Request blocked by Rheonic: ${reason}`);
|
|
9
|
+
this.name = "RHEONICBlockedError";
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class ProtectEngine {
|
|
14
|
+
baseUrl;
|
|
15
|
+
ingestKey;
|
|
16
|
+
environment;
|
|
17
|
+
fallbackRequestTimeoutMs;
|
|
18
|
+
debugLog;
|
|
19
|
+
failMode;
|
|
20
|
+
decisionTimeoutMs;
|
|
21
|
+
cooldownUntilMs;
|
|
22
|
+
cooldownReason;
|
|
23
|
+
constructor(params) {
|
|
24
|
+
this.baseUrl = params.baseUrl;
|
|
25
|
+
this.ingestKey = params.ingestKey;
|
|
26
|
+
this.environment = params.environment;
|
|
27
|
+
this.fallbackRequestTimeoutMs = params.fallbackRequestTimeoutMs;
|
|
28
|
+
this.debugLog = params.debugLog;
|
|
29
|
+
this.failMode = params.initialFailMode;
|
|
30
|
+
this.decisionTimeoutMs =
|
|
31
|
+
typeof params.initialDecisionTimeoutMs === "number" && Number.isFinite(params.initialDecisionTimeoutMs) && params.initialDecisionTimeoutMs > 0
|
|
32
|
+
? Math.floor(params.initialDecisionTimeoutMs)
|
|
33
|
+
: sdkNodeConfig.internalProtectDecisionTimeoutMs;
|
|
34
|
+
this.cooldownUntilMs = null;
|
|
35
|
+
this.cooldownReason = null;
|
|
36
|
+
}
|
|
37
|
+
async evaluate(context) {
|
|
38
|
+
const nowMs = Date.now();
|
|
39
|
+
if (this.cooldownUntilMs !== null && nowMs < this.cooldownUntilMs) {
|
|
40
|
+
this.debugLog?.("Protect preflight blocked locally from cached cooldown", {
|
|
41
|
+
provider: context.provider,
|
|
42
|
+
decision: "block",
|
|
43
|
+
reason: this.cooldownReason ?? "cooldown_active",
|
|
44
|
+
});
|
|
45
|
+
return { decision: "block", reason: this.cooldownReason ?? "cooldown_active" };
|
|
46
|
+
}
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timeoutMs = this.decisionTimeoutMs > 0 ? this.decisionTimeoutMs : this.fallbackRequestTimeoutMs;
|
|
49
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
50
|
+
timeout.unref?.();
|
|
51
|
+
const startedAt = Date.now();
|
|
52
|
+
const requestId = randomUUID();
|
|
53
|
+
const traceId = generateTraceId();
|
|
54
|
+
const spanId = generateSpanId();
|
|
55
|
+
try {
|
|
56
|
+
const response = await bindTraceContext(traceId, spanId, async () => await requestJson(`${this.baseUrl}/api/v1/protect/decision`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"X-Project-Ingest-Key": this.ingestKey,
|
|
61
|
+
"X-Trace-ID": getTraceId(),
|
|
62
|
+
"X-Span-ID": spanId,
|
|
63
|
+
"X-Rheonic-Protect-Request-Id": requestId,
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify(context),
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
}));
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
this.debugLog?.("Protect preflight returned non-success status", {
|
|
71
|
+
provider: context.provider,
|
|
72
|
+
status_code: response.status,
|
|
73
|
+
latency_ms: Date.now() - startedAt,
|
|
74
|
+
});
|
|
75
|
+
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId);
|
|
76
|
+
return this.failMode === "closed"
|
|
77
|
+
? { decision: "block", reason: "decision_unavailable" }
|
|
78
|
+
: { decision: "allow", reason: "decision_unavailable" };
|
|
79
|
+
}
|
|
80
|
+
const parsed = (await response.json());
|
|
81
|
+
const decision = parseDecision(parsed.decision);
|
|
82
|
+
const reason = typeof parsed.reason === "string" ? parsed.reason : "ok";
|
|
83
|
+
const failMode = parseFailMode(parsed.fail_mode);
|
|
84
|
+
if (failMode) {
|
|
85
|
+
this.failMode = failMode;
|
|
86
|
+
}
|
|
87
|
+
const decisionTimeout = Number(parsed.protect_decision_timeout_ms);
|
|
88
|
+
if (Number.isFinite(decisionTimeout) && decisionTimeout > 0) {
|
|
89
|
+
this.decisionTimeoutMs = decisionTimeout;
|
|
90
|
+
}
|
|
91
|
+
const blockedUntilMs = parseBlockedUntilMs(parsed.blocked_until);
|
|
92
|
+
if (blockedUntilMs !== null && blockedUntilMs > Date.now()) {
|
|
93
|
+
this.cooldownUntilMs = blockedUntilMs;
|
|
94
|
+
this.cooldownReason = "cooldown_active";
|
|
95
|
+
}
|
|
96
|
+
else if (this.cooldownUntilMs !== null && Date.now() >= this.cooldownUntilMs) {
|
|
97
|
+
this.cooldownUntilMs = null;
|
|
98
|
+
this.cooldownReason = null;
|
|
99
|
+
}
|
|
100
|
+
this.debugLog?.("Protect preflight completed", {
|
|
101
|
+
provider: context.provider,
|
|
102
|
+
decision,
|
|
103
|
+
reason,
|
|
104
|
+
latency_ms: Date.now() - startedAt,
|
|
105
|
+
timeout_ms: this.decisionTimeoutMs,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
decision,
|
|
109
|
+
reason,
|
|
110
|
+
snapshot: parseSnapshot(parsed.snapshot),
|
|
111
|
+
applyClampEnabled: typeof parsed.apply_clamp_enabled === "boolean" ? parsed.apply_clamp_enabled : undefined,
|
|
112
|
+
clamp: parseClamp(parsed.clamp),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
if (isAbortError(error)) {
|
|
118
|
+
this.debugLog?.("Protect preflight timed out", {
|
|
119
|
+
provider: context.provider,
|
|
120
|
+
latency_ms: Date.now() - startedAt,
|
|
121
|
+
timeout_ms: timeoutMs,
|
|
122
|
+
});
|
|
123
|
+
void this.reportDecisionTimeout(context.provider, typeof context.model === "string" ? context.model : undefined, requestId);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.debugLog?.("Protect preflight failed", {
|
|
127
|
+
provider: context.provider,
|
|
128
|
+
latency_ms: Date.now() - startedAt,
|
|
129
|
+
error_type: extractErrorType(error),
|
|
130
|
+
});
|
|
131
|
+
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId);
|
|
132
|
+
}
|
|
133
|
+
return this.failMode === "closed"
|
|
134
|
+
? { decision: "block", reason: "decision_unavailable" }
|
|
135
|
+
: { decision: "allow", reason: "decision_unavailable" };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async bootstrap() {
|
|
139
|
+
try {
|
|
140
|
+
const response = await requestJson(`${this.baseUrl}/api/v1/protect/config`, {
|
|
141
|
+
method: "GET",
|
|
142
|
+
headers: {
|
|
143
|
+
"X-Project-Ingest-Key": this.ingestKey,
|
|
144
|
+
"X-Trace-ID": generateTraceId(),
|
|
145
|
+
"X-Span-ID": generateSpanId(),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const parsed = (await response.json());
|
|
152
|
+
const failMode = parseFailMode(parsed.protect_fail_mode);
|
|
153
|
+
if (failMode) {
|
|
154
|
+
this.failMode = failMode;
|
|
155
|
+
}
|
|
156
|
+
const decisionTimeout = Number(parsed.protect_decision_timeout_ms);
|
|
157
|
+
if (Number.isFinite(decisionTimeout) && decisionTimeout > 0) {
|
|
158
|
+
this.decisionTimeoutMs = Math.floor(decisionTimeout);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Best effort only; keep local defaults if bootstrap fails.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async reportDecisionTimeout(provider, model, requestId) {
|
|
166
|
+
try {
|
|
167
|
+
await requestJson(`${this.baseUrl}/api/v1/protect/decision-timeout`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"X-Project-Ingest-Key": this.ingestKey,
|
|
172
|
+
"X-Trace-ID": generateTraceId(),
|
|
173
|
+
"X-Span-ID": generateSpanId(),
|
|
174
|
+
"X-Rheonic-Protect-Request-Id": requestId,
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({ environment: this.environment, provider, model, request_id: requestId }),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Swallow timeout reporting errors; protect evaluation must never throw here.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async reportDecisionUnavailable(provider, model, requestId) {
|
|
184
|
+
try {
|
|
185
|
+
await requestJson(`${this.baseUrl}/api/v1/protect/decision-unavailable`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
"X-Project-Ingest-Key": this.ingestKey,
|
|
190
|
+
"X-Trace-ID": generateTraceId(),
|
|
191
|
+
"X-Span-ID": generateSpanId(),
|
|
192
|
+
"X-Rheonic-Protect-Request-Id": requestId,
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({ environment: this.environment, provider, model, request_id: requestId }),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Swallow unavailable reporting errors; protect evaluation must never throw here.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function parseClamp(value) {
|
|
203
|
+
if (!value || typeof value !== "object") {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const candidate = value;
|
|
207
|
+
if (typeof candidate.recommended_max_output_tokens !== "number" || candidate.recommended_max_output_tokens < 1) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
recommended_max_output_tokens: Math.floor(candidate.recommended_max_output_tokens),
|
|
212
|
+
applied: typeof candidate.applied === "boolean" ? candidate.applied : false,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function parseSnapshot(value) {
|
|
216
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
function parseBlockedUntilMs(value) {
|
|
222
|
+
if (typeof value !== "string" || !value) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const parsed = Date.parse(value);
|
|
226
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return parsed;
|
|
230
|
+
}
|
|
231
|
+
function parseDecision(value) {
|
|
232
|
+
if (value === "warn" || value === "block" || value === "allow") {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
return "allow";
|
|
236
|
+
}
|
|
237
|
+
function parseFailMode(value) {
|
|
238
|
+
if (value === "open" || value === "closed") {
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function isAbortError(value) {
|
|
244
|
+
return typeof value === "object" && value !== null && "name" in value && value.name === "AbortError";
|
|
245
|
+
}
|
|
246
|
+
function extractErrorType(value) {
|
|
247
|
+
if (value && typeof value === "object" && "name" in value) {
|
|
248
|
+
const maybeName = value.name;
|
|
249
|
+
if (typeof maybeName === "string" && maybeName.length > 0) {
|
|
250
|
+
return maybeName;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return "unknown";
|
|
254
|
+
}
|
|
255
|
+
export const defaultProtectTimeoutMs = sdkNodeConfig.internalProtectDecisionTimeoutMs;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare class RHEONICValidationError extends Error {
|
|
2
|
+
readonly provider: string;
|
|
3
|
+
readonly model: string;
|
|
4
|
+
readonly expectedProviders: readonly string[];
|
|
5
|
+
constructor(message: string, provider: string, model: string, expectedProviders: readonly string[]);
|
|
6
|
+
}
|
|
7
|
+
export declare function validateProviderModel(provider: string, model: string | null | undefined): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { sdkNodeConfig } from "./config.js";
|
|
2
|
+
export class RHEONICValidationError extends Error {
|
|
3
|
+
provider;
|
|
4
|
+
model;
|
|
5
|
+
expectedProviders;
|
|
6
|
+
constructor(message, provider, model, expectedProviders) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "RHEONICValidationError";
|
|
9
|
+
this.provider = provider;
|
|
10
|
+
this.model = model;
|
|
11
|
+
this.expectedProviders = expectedProviders;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function validateProviderModel(provider, model) {
|
|
15
|
+
const normalizedProvider = provider.trim().toLowerCase();
|
|
16
|
+
if (!normalizedProvider) {
|
|
17
|
+
throw new RHEONICValidationError("RHEONIC: provider must be explicitly provided.", provider, String(model ?? ""), sdkNodeConfig.supportedProviders);
|
|
18
|
+
}
|
|
19
|
+
if (!sdkNodeConfig.supportedProviders.includes(normalizedProvider)) {
|
|
20
|
+
throw new RHEONICValidationError(`RHEONIC: unsupported provider: ${provider}`, provider, String(model ?? ""), sdkNodeConfig.supportedProviders);
|
|
21
|
+
}
|
|
22
|
+
const normalizedModel = typeof model === "string" ? model.trim() : "";
|
|
23
|
+
if (!normalizedModel) {
|
|
24
|
+
throw new RHEONICValidationError(`RHEONIC: model must be explicitly provided for provider ${normalizedProvider}.`, normalizedProvider, String(model ?? ""), sdkNodeConfig.supportedProviders);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from "../client.js";
|
|
2
|
+
export interface AnthropicInstrumentationOptions {
|
|
3
|
+
client: Client;
|
|
4
|
+
environment?: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
feature?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function __setInputTokenEstimatorForTests(estimator: ((payload: unknown) => number | null) | null): void;
|
|
9
|
+
export declare function instrumentAnthropic<T extends Record<string, any>>(anthropicClient: T, options: AnthropicInstrumentationOptions): T;
|