@rheonic/sdk 0.1.0-beta.7 → 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.d.ts +1 -0
- package/dist/client.js +27 -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.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
|
@@ -98,6 +98,10 @@ export class Client {
|
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
async captureEventAndFlush(event) {
|
|
102
|
+
await this.captureEvent(event);
|
|
103
|
+
await this.flushWithTimeout();
|
|
104
|
+
}
|
|
101
105
|
getStats() {
|
|
102
106
|
return {
|
|
103
107
|
queued: this.queue.length,
|
|
@@ -126,7 +130,11 @@ export class Client {
|
|
|
126
130
|
}
|
|
127
131
|
async evaluateProtectDecision(context) {
|
|
128
132
|
await this.ensureWarmup();
|
|
129
|
-
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
|
+
});
|
|
130
138
|
}
|
|
131
139
|
async warmConnections() {
|
|
132
140
|
if (this.warmupCompleted) {
|
|
@@ -152,21 +160,29 @@ export class Client {
|
|
|
152
160
|
}
|
|
153
161
|
async runWarmup() {
|
|
154
162
|
try {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
});
|
|
163
175
|
}
|
|
164
176
|
catch {
|
|
165
177
|
this.debugLog("SDK connection warmup failed");
|
|
166
178
|
}
|
|
167
179
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
});
|
|
170
186
|
}
|
|
171
187
|
catch {
|
|
172
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
|
-
void options.client.captureEvent(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
|
-
void options.client.captureEvent(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
|
-
void options.client.captureEvent(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
|
-
void options.client.captureEvent(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
|
-
void options.client.captureEvent(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
|
-
void options.client.captureEvent(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