@rheonic/sdk 0.1.0-beta.8 → 0.1.0-beta.9
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 +3 -1
- package/dist/client.js +23 -11
- package/dist/logger.js +4 -2
- package/dist/protectEngine.d.ts +2 -0
- package/dist/protectEngine.js +100 -98
- package/dist/providers/anthropicAdapter.js +79 -72
- package/dist/providers/googleAdapter.js +78 -71
- package/dist/providers/openaiAdapter.js +78 -71
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,9 @@ All notable changes to `@rheonic/sdk` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### Fixed
|
|
8
|
+
- SDK debug logs now always emit non-empty `trace_id` and `span_id`, including warmup, token-estimation, and protect preflight paths.
|
|
9
|
+
- Provider instrumentation now keeps the full protected call lifecycle under one trace so SDK debug logs correlate cleanly with backend requests.
|
|
8
10
|
|
|
9
11
|
## 0.1.0-beta.7
|
|
10
12
|
|
package/dist/client.js
CHANGED
|
@@ -130,7 +130,11 @@ export class Client {
|
|
|
130
130
|
}
|
|
131
131
|
async evaluateProtectDecision(context) {
|
|
132
132
|
await this.ensureWarmup();
|
|
133
|
-
return this.protectEngine.evaluate(
|
|
133
|
+
return this.protectEngine.evaluate({
|
|
134
|
+
...context,
|
|
135
|
+
trace_id: context.trace_id ?? getTraceId() ?? undefined,
|
|
136
|
+
span_id: context.span_id ?? getSpanId() ?? undefined,
|
|
137
|
+
});
|
|
134
138
|
}
|
|
135
139
|
async warmConnections() {
|
|
136
140
|
if (this.warmupCompleted) {
|
|
@@ -156,21 +160,29 @@ export class Client {
|
|
|
156
160
|
}
|
|
157
161
|
async runWarmup() {
|
|
158
162
|
try {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
const traceId = generateTraceId();
|
|
164
|
+
const spanId = generateSpanId();
|
|
165
|
+
await bindTraceContext(traceId, spanId, async () => {
|
|
166
|
+
const response = await requestJson(`${this.baseUrl}/health`, {
|
|
167
|
+
method: "GET",
|
|
168
|
+
headers: {
|
|
169
|
+
"X-Trace-ID": getTraceId(),
|
|
170
|
+
"X-Span-ID": getSpanId(),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
this.debugLog("SDK connection warmup completed", { status_code: response.status });
|
|
174
|
+
});
|
|
167
175
|
}
|
|
168
176
|
catch {
|
|
169
177
|
this.debugLog("SDK connection warmup failed");
|
|
170
178
|
}
|
|
171
179
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
const traceId = generateTraceId();
|
|
181
|
+
const spanId = generateSpanId();
|
|
182
|
+
await bindTraceContext(traceId, spanId, async () => {
|
|
183
|
+
await this.protectEngine.bootstrap();
|
|
184
|
+
this.debugLog("SDK protect config bootstrap completed");
|
|
185
|
+
});
|
|
174
186
|
}
|
|
175
187
|
catch {
|
|
176
188
|
this.debugLog("SDK protect config bootstrap failed");
|
package/dist/logger.js
CHANGED
|
@@ -19,6 +19,8 @@ export function getSpanId() {
|
|
|
19
19
|
return contextStorage.getStore()?.spanId ?? "";
|
|
20
20
|
}
|
|
21
21
|
export function emitLog(params) {
|
|
22
|
+
const traceId = params.traceId ?? getTraceId();
|
|
23
|
+
const spanId = params.spanId ?? getSpanId();
|
|
22
24
|
const payload = {
|
|
23
25
|
timestamp: new Date().toISOString(),
|
|
24
26
|
level: params.level,
|
|
@@ -29,8 +31,8 @@ export function emitLog(params) {
|
|
|
29
31
|
process.env.ENVIRONMENT ??
|
|
30
32
|
process.env.ENV ??
|
|
31
33
|
"unknown").toLowerCase(),
|
|
32
|
-
trace_id:
|
|
33
|
-
span_id:
|
|
34
|
+
trace_id: traceId || generateTraceId(),
|
|
35
|
+
span_id: spanId || generateSpanId(),
|
|
34
36
|
event: sanitizeEvent(params.event),
|
|
35
37
|
message: params.message,
|
|
36
38
|
metadata: sanitizeMetadata(params.metadata ?? {}),
|
package/dist/protectEngine.d.ts
CHANGED
package/dist/protectEngine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { sdkNodeConfig } from "./config.js";
|
|
3
3
|
import { requestJson } from "./httpTransport.js";
|
|
4
|
-
import { bindTraceContext, generateSpanId, generateTraceId, getTraceId } from "./logger.js";
|
|
4
|
+
import { bindTraceContext, generateSpanId, generateTraceId, getSpanId, getTraceId } from "./logger.js";
|
|
5
5
|
export class RHEONICBlockedError extends Error {
|
|
6
6
|
reason;
|
|
7
7
|
trace_id;
|
|
@@ -46,111 +46,113 @@ export class ProtectEngine {
|
|
|
46
46
|
}
|
|
47
47
|
async evaluate(context) {
|
|
48
48
|
const requestId = randomUUID();
|
|
49
|
-
const traceId = generateTraceId();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
reason: this.cooldownReason ?? "cooldown_active",
|
|
56
|
-
});
|
|
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
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const controller = new AbortController();
|
|
67
|
-
const timeoutMs = this.decisionTimeoutMs > 0 ? this.decisionTimeoutMs : this.fallbackRequestTimeoutMs;
|
|
68
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
69
|
-
timeout.unref?.();
|
|
70
|
-
const startedAt = Date.now();
|
|
71
|
-
const spanId = generateSpanId();
|
|
72
|
-
try {
|
|
73
|
-
const response = await bindTraceContext(traceId, spanId, async () => await requestJson(`${this.baseUrl}/api/v1/protect/decision`, {
|
|
74
|
-
method: "POST",
|
|
75
|
-
headers: {
|
|
76
|
-
"Content-Type": "application/json",
|
|
77
|
-
"X-Project-Ingest-Key": this.ingestKey,
|
|
78
|
-
"X-Trace-ID": getTraceId(),
|
|
79
|
-
"X-Span-ID": spanId,
|
|
80
|
-
"X-Rheonic-Protect-Request-Id": requestId,
|
|
81
|
-
},
|
|
82
|
-
body: JSON.stringify(context),
|
|
83
|
-
signal: controller.signal,
|
|
84
|
-
}));
|
|
85
|
-
clearTimeout(timeout);
|
|
86
|
-
if (!response.ok) {
|
|
87
|
-
this.debugLog?.("Protect preflight returned non-success status", {
|
|
49
|
+
const traceId = context.trace_id?.trim() || generateTraceId();
|
|
50
|
+
const spanId = context.span_id?.trim() || generateSpanId();
|
|
51
|
+
return bindTraceContext(traceId, spanId, async () => {
|
|
52
|
+
const nowMs = Date.now();
|
|
53
|
+
if (this.cooldownUntilMs !== null && nowMs < this.cooldownUntilMs) {
|
|
54
|
+
this.debugLog?.("Protect preflight blocked locally from cached cooldown", {
|
|
88
55
|
provider: context.provider,
|
|
89
|
-
|
|
90
|
-
|
|
56
|
+
decision: "block",
|
|
57
|
+
reason: this.cooldownReason ?? "cooldown_active",
|
|
91
58
|
});
|
|
92
|
-
|
|
93
|
-
|
|
59
|
+
return {
|
|
60
|
+
decision: "block",
|
|
61
|
+
reason: this.cooldownReason ?? "cooldown_active",
|
|
62
|
+
trace_id: traceId,
|
|
63
|
+
request_id: requestId,
|
|
64
|
+
blocked_until: formatBlockedUntilMs(this.cooldownUntilMs),
|
|
65
|
+
retry_after_seconds: toRetryAfterSeconds(this.cooldownUntilMs, nowMs),
|
|
66
|
+
};
|
|
94
67
|
}
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.cooldownUntilMs = null;
|
|
113
|
-
this.cooldownReason = null;
|
|
114
|
-
}
|
|
115
|
-
this.debugLog?.("Protect preflight completed", {
|
|
116
|
-
provider: context.provider,
|
|
117
|
-
decision,
|
|
118
|
-
reason,
|
|
119
|
-
latency_ms: Date.now() - startedAt,
|
|
120
|
-
timeout_ms: this.decisionTimeoutMs,
|
|
121
|
-
});
|
|
122
|
-
return {
|
|
123
|
-
decision,
|
|
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),
|
|
129
|
-
snapshot: parseSnapshot(parsed.snapshot),
|
|
130
|
-
applyClampEnabled: typeof parsed.apply_clamp_enabled === "boolean" ? parsed.apply_clamp_enabled : undefined,
|
|
131
|
-
clamp: parseClamp(parsed.clamp),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
clearTimeout(timeout);
|
|
136
|
-
if (isAbortError(error)) {
|
|
137
|
-
this.debugLog?.("Protect preflight timed out", {
|
|
138
|
-
provider: context.provider,
|
|
139
|
-
latency_ms: Date.now() - startedAt,
|
|
140
|
-
timeout_ms: timeoutMs,
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timeoutMs = this.decisionTimeoutMs > 0 ? this.decisionTimeoutMs : this.fallbackRequestTimeoutMs;
|
|
70
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
71
|
+
timeout.unref?.();
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
try {
|
|
74
|
+
const response = await requestJson(`${this.baseUrl}/api/v1/protect/decision`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
"X-Project-Ingest-Key": this.ingestKey,
|
|
79
|
+
"X-Trace-ID": getTraceId(),
|
|
80
|
+
"X-Span-ID": getSpanId(),
|
|
81
|
+
"X-Rheonic-Protect-Request-Id": requestId,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(context),
|
|
84
|
+
signal: controller.signal,
|
|
141
85
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
this.debugLog?.("Protect preflight returned non-success status", {
|
|
89
|
+
provider: context.provider,
|
|
90
|
+
status_code: response.status,
|
|
91
|
+
latency_ms: Date.now() - startedAt,
|
|
92
|
+
});
|
|
93
|
+
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
|
|
94
|
+
return this.fallbackEvaluation(traceId, requestId);
|
|
95
|
+
}
|
|
96
|
+
const parsed = (await response.json());
|
|
97
|
+
const decision = parseDecision(parsed.decision);
|
|
98
|
+
const reason = typeof parsed.reason === "string" ? parsed.reason : "ok";
|
|
99
|
+
const failMode = parseFailMode(parsed.fail_mode);
|
|
100
|
+
if (failMode) {
|
|
101
|
+
this.failMode = failMode;
|
|
102
|
+
}
|
|
103
|
+
const decisionTimeout = Number(parsed.protect_decision_timeout_ms);
|
|
104
|
+
if (Number.isFinite(decisionTimeout) && decisionTimeout > 0) {
|
|
105
|
+
this.decisionTimeoutMs = decisionTimeout;
|
|
106
|
+
}
|
|
107
|
+
const blockedUntilMs = parseBlockedUntilMs(parsed.blocked_until);
|
|
108
|
+
if (blockedUntilMs !== null && blockedUntilMs > Date.now()) {
|
|
109
|
+
this.cooldownUntilMs = blockedUntilMs;
|
|
110
|
+
this.cooldownReason = "cooldown_active";
|
|
111
|
+
}
|
|
112
|
+
else if (this.cooldownUntilMs !== null && Date.now() >= this.cooldownUntilMs) {
|
|
113
|
+
this.cooldownUntilMs = null;
|
|
114
|
+
this.cooldownReason = null;
|
|
115
|
+
}
|
|
116
|
+
this.debugLog?.("Protect preflight completed", {
|
|
146
117
|
provider: context.provider,
|
|
118
|
+
decision,
|
|
119
|
+
reason,
|
|
147
120
|
latency_ms: Date.now() - startedAt,
|
|
148
|
-
|
|
121
|
+
timeout_ms: this.decisionTimeoutMs,
|
|
149
122
|
});
|
|
150
|
-
|
|
123
|
+
return {
|
|
124
|
+
decision,
|
|
125
|
+
reason,
|
|
126
|
+
trace_id: traceId,
|
|
127
|
+
request_id: requestId,
|
|
128
|
+
blocked_until: typeof parsed.blocked_until === "string" ? parsed.blocked_until : undefined,
|
|
129
|
+
retry_after_seconds: parseRetryAfterSeconds(parsed.retry_after_seconds),
|
|
130
|
+
snapshot: parseSnapshot(parsed.snapshot),
|
|
131
|
+
applyClampEnabled: typeof parsed.apply_clamp_enabled === "boolean" ? parsed.apply_clamp_enabled : undefined,
|
|
132
|
+
clamp: parseClamp(parsed.clamp),
|
|
133
|
+
};
|
|
151
134
|
}
|
|
152
|
-
|
|
153
|
-
|
|
135
|
+
catch (error) {
|
|
136
|
+
clearTimeout(timeout);
|
|
137
|
+
if (isAbortError(error)) {
|
|
138
|
+
this.debugLog?.("Protect preflight timed out", {
|
|
139
|
+
provider: context.provider,
|
|
140
|
+
latency_ms: Date.now() - startedAt,
|
|
141
|
+
timeout_ms: timeoutMs,
|
|
142
|
+
});
|
|
143
|
+
void this.reportDecisionTimeout(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.debugLog?.("Protect preflight failed", {
|
|
147
|
+
provider: context.provider,
|
|
148
|
+
latency_ms: Date.now() - startedAt,
|
|
149
|
+
error_type: extractErrorType(error),
|
|
150
|
+
});
|
|
151
|
+
void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
|
|
152
|
+
}
|
|
153
|
+
return this.fallbackEvaluation(traceId, requestId);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
154
156
|
}
|
|
155
157
|
fallbackEvaluation(traceId, requestId) {
|
|
156
158
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildEvent } from "../eventBuilder.js";
|
|
2
|
+
import { bindTraceContext, generateSpanId, generateTraceId } from "../logger.js";
|
|
2
3
|
import { RHEONICBlockedError } from "../protectEngine.js";
|
|
3
4
|
import { validateProviderModel } from "../providerModelValidation.js";
|
|
4
5
|
import { estimateInputTokensFromRequest } from "../tokenEstimator.js";
|
|
@@ -13,82 +14,88 @@ export function instrumentAnthropic(anthropicClient, options) {
|
|
|
13
14
|
}
|
|
14
15
|
const originalCreate = targetCreate.bind(anthropicClient.messages);
|
|
15
16
|
anthropicClient.messages.create = async (...args) => {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
estimated_input_tokens: estimatedInputTokens ?? undefined,
|
|
32
|
-
});
|
|
33
|
-
const protectPayload = {
|
|
34
|
-
provider: "anthropic",
|
|
35
|
-
model: requestedModel,
|
|
36
|
-
environment: options.environment ?? options.client.environment,
|
|
37
|
-
feature: options.feature,
|
|
38
|
-
max_output_tokens: extractMaxOutputTokens(args),
|
|
39
|
-
};
|
|
40
|
-
if (typeof estimatedInputTokens === "number") {
|
|
41
|
-
protectPayload.input_tokens_estimate = estimatedInputTokens;
|
|
42
|
-
}
|
|
43
|
-
const protectDecision = await options.client.evaluateProtectDecision(protectPayload);
|
|
44
|
-
if (protectDecision.decision === "block") {
|
|
45
|
-
throw new RHEONICBlockedError(protectDecision);
|
|
46
|
-
}
|
|
47
|
-
const callArgs = maybeApplyAnthropicClamp(args, protectDecision);
|
|
48
|
-
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
49
|
-
try {
|
|
50
|
-
const response = await originalCreate(...callArgs);
|
|
51
|
-
await options.client.captureEventAndFlush(buildEvent({
|
|
17
|
+
const traceId = generateTraceId();
|
|
18
|
+
const spanId = generateSpanId();
|
|
19
|
+
return bindTraceContext(traceId, spanId, async () => {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
const requestPayload = extractRequestPayload(args);
|
|
22
|
+
const requestedModel = extractRequestedModel(args);
|
|
23
|
+
validateProviderModel("anthropic", requestedModel);
|
|
24
|
+
let estimatedInputTokens = null;
|
|
25
|
+
const tokenEstimateStartedAt = Date.now();
|
|
26
|
+
estimatedInputTokens = requestPayload
|
|
27
|
+
? (estimatorOverrideForTests
|
|
28
|
+
? estimatorOverrideForTests(requestPayload)
|
|
29
|
+
: estimateInputTokensFromRequest(requestPayload))
|
|
30
|
+
: null;
|
|
31
|
+
options.client.debugLog("Protect token estimation completed", {
|
|
52
32
|
provider: "anthropic",
|
|
53
|
-
model:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
59
|
-
input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
60
|
-
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
61
|
-
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
62
|
-
},
|
|
63
|
-
response: {
|
|
64
|
-
latency_ms: Date.now() - startedAt,
|
|
65
|
-
total_tokens: extractTotalTokens(response),
|
|
66
|
-
http_status: 200,
|
|
67
|
-
},
|
|
68
|
-
}));
|
|
69
|
-
return response;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
await options.client.captureEventAndFlush(buildEvent({
|
|
33
|
+
model: requestedModel,
|
|
34
|
+
latency_ms: Date.now() - tokenEstimateStartedAt,
|
|
35
|
+
estimated_input_tokens: estimatedInputTokens ?? undefined,
|
|
36
|
+
});
|
|
37
|
+
const protectPayload = {
|
|
73
38
|
provider: "anthropic",
|
|
74
39
|
model: requestedModel,
|
|
75
40
|
environment: options.environment ?? options.client.environment,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
41
|
+
feature: options.feature,
|
|
42
|
+
max_output_tokens: extractMaxOutputTokens(args),
|
|
43
|
+
trace_id: traceId,
|
|
44
|
+
span_id: spanId,
|
|
45
|
+
};
|
|
46
|
+
if (typeof estimatedInputTokens === "number") {
|
|
47
|
+
protectPayload.input_tokens_estimate = estimatedInputTokens;
|
|
48
|
+
}
|
|
49
|
+
const protectDecision = await options.client.evaluateProtectDecision(protectPayload);
|
|
50
|
+
if (protectDecision.decision === "block") {
|
|
51
|
+
throw new RHEONICBlockedError(protectDecision);
|
|
52
|
+
}
|
|
53
|
+
const callArgs = maybeApplyAnthropicClamp(args, protectDecision);
|
|
54
|
+
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
55
|
+
try {
|
|
56
|
+
const response = await originalCreate(...callArgs);
|
|
57
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
58
|
+
provider: "anthropic",
|
|
59
|
+
model: extractResponseModel(response) ?? requestedModel,
|
|
60
|
+
environment: options.environment ?? options.client.environment,
|
|
61
|
+
request: {
|
|
62
|
+
endpoint: options.endpoint,
|
|
63
|
+
feature: options.feature,
|
|
64
|
+
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
65
|
+
input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
66
|
+
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
67
|
+
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
68
|
+
},
|
|
69
|
+
response: {
|
|
70
|
+
latency_ms: Date.now() - startedAt,
|
|
71
|
+
total_tokens: extractTotalTokens(response),
|
|
72
|
+
http_status: 200,
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
79
|
+
provider: "anthropic",
|
|
80
|
+
model: requestedModel,
|
|
81
|
+
environment: options.environment ?? options.client.environment,
|
|
82
|
+
request: {
|
|
83
|
+
endpoint: options.endpoint,
|
|
84
|
+
feature: options.feature,
|
|
85
|
+
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
86
|
+
input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
87
|
+
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
88
|
+
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
89
|
+
},
|
|
90
|
+
response: {
|
|
91
|
+
latency_ms: Date.now() - startedAt,
|
|
92
|
+
error_type: extractErrorType(error),
|
|
93
|
+
http_status: extractHttpStatus(error),
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
92
99
|
};
|
|
93
100
|
return anthropicClient;
|
|
94
101
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildEvent } from "../eventBuilder.js";
|
|
2
|
+
import { bindTraceContext, generateSpanId, generateTraceId } from "../logger.js";
|
|
2
3
|
import { RHEONICBlockedError } from "../protectEngine.js";
|
|
3
4
|
import { validateProviderModel } from "../providerModelValidation.js";
|
|
4
5
|
import { estimateInputTokensFromRequest } from "../tokenEstimator.js";
|
|
@@ -13,82 +14,88 @@ export function instrumentGoogle(googleModel, options) {
|
|
|
13
14
|
}
|
|
14
15
|
const originalGenerate = targetGenerate.bind(googleModel);
|
|
15
16
|
googleModel.generateContent = async (...args) => {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
estimated_input_tokens: estimatedInputTokens ?? undefined,
|
|
32
|
-
});
|
|
33
|
-
const protectPayload = {
|
|
34
|
-
provider: "google",
|
|
35
|
-
model: requestedModel,
|
|
36
|
-
environment: options.environment ?? options.client.environment,
|
|
37
|
-
feature: options.feature,
|
|
38
|
-
max_output_tokens: extractMaxOutputTokens(args),
|
|
39
|
-
};
|
|
40
|
-
if (typeof estimatedInputTokens === "number") {
|
|
41
|
-
protectPayload.input_tokens_estimate = estimatedInputTokens;
|
|
42
|
-
}
|
|
43
|
-
const protectDecision = await options.client.evaluateProtectDecision(protectPayload);
|
|
44
|
-
if (protectDecision.decision === "block") {
|
|
45
|
-
throw new RHEONICBlockedError(protectDecision);
|
|
46
|
-
}
|
|
47
|
-
const callArgs = maybeApplyGoogleClamp(args, protectDecision);
|
|
48
|
-
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
49
|
-
try {
|
|
50
|
-
const response = await originalGenerate(...callArgs);
|
|
51
|
-
await options.client.captureEventAndFlush(buildEvent({
|
|
17
|
+
const traceId = generateTraceId();
|
|
18
|
+
const spanId = generateSpanId();
|
|
19
|
+
return bindTraceContext(traceId, spanId, async () => {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
const requestedModel = extractRequestedModel(googleModel);
|
|
22
|
+
validateProviderModel("google", requestedModel);
|
|
23
|
+
const requestPayload = extractRequestPayload(args, requestedModel);
|
|
24
|
+
let estimatedInputTokens = null;
|
|
25
|
+
const tokenEstimateStartedAt = Date.now();
|
|
26
|
+
estimatedInputTokens = requestPayload
|
|
27
|
+
? (estimatorOverrideForTests
|
|
28
|
+
? estimatorOverrideForTests(requestPayload)
|
|
29
|
+
: estimateInputTokensFromRequest(requestPayload))
|
|
30
|
+
: null;
|
|
31
|
+
options.client.debugLog("Protect token estimation completed", {
|
|
52
32
|
provider: "google",
|
|
53
33
|
model: requestedModel,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
59
|
-
input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
60
|
-
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
61
|
-
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
62
|
-
},
|
|
63
|
-
response: {
|
|
64
|
-
latency_ms: Date.now() - startedAt,
|
|
65
|
-
total_tokens: extractTotalTokens(response),
|
|
66
|
-
http_status: 200,
|
|
67
|
-
},
|
|
68
|
-
}));
|
|
69
|
-
return response;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
await options.client.captureEventAndFlush(buildEvent({
|
|
34
|
+
latency_ms: Date.now() - tokenEstimateStartedAt,
|
|
35
|
+
estimated_input_tokens: estimatedInputTokens ?? undefined,
|
|
36
|
+
});
|
|
37
|
+
const protectPayload = {
|
|
73
38
|
provider: "google",
|
|
74
39
|
model: requestedModel,
|
|
75
40
|
environment: options.environment ?? options.client.environment,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
41
|
+
feature: options.feature,
|
|
42
|
+
max_output_tokens: extractMaxOutputTokens(args),
|
|
43
|
+
trace_id: traceId,
|
|
44
|
+
span_id: spanId,
|
|
45
|
+
};
|
|
46
|
+
if (typeof estimatedInputTokens === "number") {
|
|
47
|
+
protectPayload.input_tokens_estimate = estimatedInputTokens;
|
|
48
|
+
}
|
|
49
|
+
const protectDecision = await options.client.evaluateProtectDecision(protectPayload);
|
|
50
|
+
if (protectDecision.decision === "block") {
|
|
51
|
+
throw new RHEONICBlockedError(protectDecision);
|
|
52
|
+
}
|
|
53
|
+
const callArgs = maybeApplyGoogleClamp(args, protectDecision);
|
|
54
|
+
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
55
|
+
try {
|
|
56
|
+
const response = await originalGenerate(...callArgs);
|
|
57
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
58
|
+
provider: "google",
|
|
59
|
+
model: requestedModel,
|
|
60
|
+
environment: options.environment ?? options.client.environment,
|
|
61
|
+
request: {
|
|
62
|
+
endpoint: options.endpoint,
|
|
63
|
+
feature: options.feature,
|
|
64
|
+
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
65
|
+
input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
66
|
+
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
67
|
+
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
68
|
+
},
|
|
69
|
+
response: {
|
|
70
|
+
latency_ms: Date.now() - startedAt,
|
|
71
|
+
total_tokens: extractTotalTokens(response),
|
|
72
|
+
http_status: 200,
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
79
|
+
provider: "google",
|
|
80
|
+
model: requestedModel,
|
|
81
|
+
environment: options.environment ?? options.client.environment,
|
|
82
|
+
request: {
|
|
83
|
+
endpoint: options.endpoint,
|
|
84
|
+
feature: options.feature,
|
|
85
|
+
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
86
|
+
input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
87
|
+
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
88
|
+
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
89
|
+
},
|
|
90
|
+
response: {
|
|
91
|
+
latency_ms: Date.now() - startedAt,
|
|
92
|
+
error_type: extractErrorType(error),
|
|
93
|
+
http_status: extractHttpStatus(error),
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
92
99
|
};
|
|
93
100
|
return googleModel;
|
|
94
101
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildEvent } from "../eventBuilder.js";
|
|
2
|
+
import { bindTraceContext, generateSpanId, generateTraceId } from "../logger.js";
|
|
2
3
|
import { RHEONICBlockedError } from "../protectEngine.js";
|
|
3
4
|
import { validateProviderModel } from "../providerModelValidation.js";
|
|
4
5
|
import { estimateInputTokensFromRequest } from "../tokenEstimator.js";
|
|
@@ -13,81 +14,87 @@ export function instrumentOpenAI(openaiClient, options) {
|
|
|
13
14
|
}
|
|
14
15
|
const originalCreate = targetCreate.bind(openaiClient.chat.completions);
|
|
15
16
|
openaiClient.chat.completions.create = async (...args) => {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
estimated_input_tokens: estimatedInputTokens ?? undefined,
|
|
31
|
-
});
|
|
32
|
-
const protectPayload = {
|
|
33
|
-
provider: "openai",
|
|
34
|
-
model,
|
|
35
|
-
environment: options.environment ?? options.client.environment,
|
|
36
|
-
feature: options.feature,
|
|
37
|
-
max_output_tokens: extractMaxOutputTokens(args),
|
|
38
|
-
};
|
|
39
|
-
if (typeof estimatedInputTokens === "number") {
|
|
40
|
-
protectPayload.input_tokens_estimate = estimatedInputTokens;
|
|
41
|
-
}
|
|
42
|
-
const protectDecision = await options.client.evaluateProtectDecision({
|
|
43
|
-
...protectPayload,
|
|
44
|
-
});
|
|
45
|
-
if (protectDecision.decision === "block") {
|
|
46
|
-
throw new RHEONICBlockedError(protectDecision);
|
|
47
|
-
}
|
|
48
|
-
const callArgs = maybeApplyOpenAIClamp(args, protectDecision);
|
|
49
|
-
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
50
|
-
try {
|
|
51
|
-
const response = await originalCreate(...callArgs);
|
|
52
|
-
await options.client.captureEventAndFlush(buildEvent({
|
|
17
|
+
const traceId = generateTraceId();
|
|
18
|
+
const spanId = generateSpanId();
|
|
19
|
+
return bindTraceContext(traceId, spanId, async () => {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
const model = extractRequestedModel(args);
|
|
22
|
+
validateProviderModel("openai", model);
|
|
23
|
+
const requestPayload = extractRequestPayload(args);
|
|
24
|
+
const tokenEstimateStartedAt = Date.now();
|
|
25
|
+
const estimatedInputTokens = requestPayload
|
|
26
|
+
? (estimatorOverrideForTests
|
|
27
|
+
? estimatorOverrideForTests(requestPayload)
|
|
28
|
+
: estimateInputTokensFromRequest(requestPayload))
|
|
29
|
+
: null;
|
|
30
|
+
options.client.debugLog("Protect token estimation completed", {
|
|
53
31
|
provider: "openai",
|
|
54
|
-
model
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
60
|
-
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
61
|
-
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
62
|
-
},
|
|
63
|
-
response: {
|
|
64
|
-
latency_ms: Date.now() - startedAt,
|
|
65
|
-
total_tokens: extractTotalTokens(response),
|
|
66
|
-
http_status: 200,
|
|
67
|
-
},
|
|
68
|
-
}));
|
|
69
|
-
return response;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
await options.client.captureEventAndFlush(buildEvent({
|
|
32
|
+
model,
|
|
33
|
+
latency_ms: Date.now() - tokenEstimateStartedAt,
|
|
34
|
+
estimated_input_tokens: estimatedInputTokens ?? undefined,
|
|
35
|
+
});
|
|
36
|
+
const protectPayload = {
|
|
73
37
|
provider: "openai",
|
|
74
38
|
model,
|
|
75
39
|
environment: options.environment ?? options.client.environment,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
40
|
+
feature: options.feature,
|
|
41
|
+
max_output_tokens: extractMaxOutputTokens(args),
|
|
42
|
+
trace_id: traceId,
|
|
43
|
+
span_id: spanId,
|
|
44
|
+
};
|
|
45
|
+
if (typeof estimatedInputTokens === "number") {
|
|
46
|
+
protectPayload.input_tokens_estimate = estimatedInputTokens;
|
|
47
|
+
}
|
|
48
|
+
const protectDecision = await options.client.evaluateProtectDecision({
|
|
49
|
+
...protectPayload,
|
|
50
|
+
});
|
|
51
|
+
if (protectDecision.decision === "block") {
|
|
52
|
+
throw new RHEONICBlockedError(protectDecision);
|
|
53
|
+
}
|
|
54
|
+
const callArgs = maybeApplyOpenAIClamp(args, protectDecision);
|
|
55
|
+
markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
|
|
56
|
+
try {
|
|
57
|
+
const response = await originalCreate(...callArgs);
|
|
58
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
59
|
+
provider: "openai",
|
|
60
|
+
model: extractResponseModel(response) ?? model,
|
|
61
|
+
environment: options.environment ?? options.client.environment,
|
|
62
|
+
request: {
|
|
63
|
+
endpoint: options.endpoint,
|
|
64
|
+
feature: options.feature,
|
|
65
|
+
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
66
|
+
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
67
|
+
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
68
|
+
},
|
|
69
|
+
response: {
|
|
70
|
+
latency_ms: Date.now() - startedAt,
|
|
71
|
+
total_tokens: extractTotalTokens(response),
|
|
72
|
+
http_status: 200,
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
await options.client.captureEventAndFlush(buildEvent({
|
|
79
|
+
provider: "openai",
|
|
80
|
+
model,
|
|
81
|
+
environment: options.environment ?? options.client.environment,
|
|
82
|
+
request: {
|
|
83
|
+
endpoint: options.endpoint,
|
|
84
|
+
feature: options.feature,
|
|
85
|
+
token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
|
|
86
|
+
protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
|
|
87
|
+
protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
|
|
88
|
+
},
|
|
89
|
+
response: {
|
|
90
|
+
latency_ms: Date.now() - startedAt,
|
|
91
|
+
error_type: extractErrorType(error),
|
|
92
|
+
http_status: extractHttpStatus(error),
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
91
98
|
};
|
|
92
99
|
return openaiClient;
|
|
93
100
|
}
|
package/package.json
CHANGED