@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 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
- - Publish-ready changelog entries will be added here for the next release.
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(context);
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 response = await bindTraceContext(generateTraceId(), generateSpanId(), async () => await requestJson(`${this.baseUrl}/health`, {
160
- method: "GET",
161
- headers: {
162
- "X-Trace-ID": getTraceId(),
163
- "X-Span-ID": getSpanId(),
164
- },
165
- }));
166
- this.debugLog("SDK connection warmup completed", { status_code: response.status });
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
- await this.protectEngine.bootstrap();
173
- this.debugLog("SDK protect config bootstrap completed");
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: params.traceId ?? getTraceId(),
33
- span_id: params.spanId ?? getSpanId(),
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 ?? {}),
@@ -6,6 +6,8 @@ export interface ProtectContext {
6
6
  feature?: string;
7
7
  max_output_tokens?: number;
8
8
  input_tokens_estimate?: number;
9
+ trace_id?: string;
10
+ span_id?: string;
9
11
  }
10
12
  export interface ProtectEvaluation {
11
13
  decision: ProtectDecision;
@@ -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 nowMs = Date.now();
51
- if (this.cooldownUntilMs !== null && nowMs < this.cooldownUntilMs) {
52
- this.debugLog?.("Protect preflight blocked locally from cached cooldown", {
53
- provider: context.provider,
54
- decision: "block",
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
- status_code: response.status,
90
- latency_ms: Date.now() - startedAt,
56
+ decision: "block",
57
+ reason: this.cooldownReason ?? "cooldown_active",
91
58
  });
92
- void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
93
- return this.fallbackEvaluation(traceId, requestId);
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 parsed = (await response.json());
96
- const decision = parseDecision(parsed.decision);
97
- const reason = typeof parsed.reason === "string" ? parsed.reason : "ok";
98
- const failMode = parseFailMode(parsed.fail_mode);
99
- if (failMode) {
100
- this.failMode = failMode;
101
- }
102
- const decisionTimeout = Number(parsed.protect_decision_timeout_ms);
103
- if (Number.isFinite(decisionTimeout) && decisionTimeout > 0) {
104
- this.decisionTimeoutMs = decisionTimeout;
105
- }
106
- const blockedUntilMs = parseBlockedUntilMs(parsed.blocked_until);
107
- if (blockedUntilMs !== null && blockedUntilMs > Date.now()) {
108
- this.cooldownUntilMs = blockedUntilMs;
109
- this.cooldownReason = "cooldown_active";
110
- }
111
- else if (this.cooldownUntilMs !== null && Date.now() >= this.cooldownUntilMs) {
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
- void this.reportDecisionTimeout(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
143
- }
144
- else {
145
- this.debugLog?.("Protect preflight failed", {
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
- error_type: extractErrorType(error),
121
+ timeout_ms: this.decisionTimeoutMs,
149
122
  });
150
- void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
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
- return this.fallbackEvaluation(traceId, requestId);
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 startedAt = Date.now();
17
- const requestPayload = extractRequestPayload(args);
18
- const requestedModel = extractRequestedModel(args);
19
- validateProviderModel("anthropic", requestedModel);
20
- let estimatedInputTokens = null;
21
- const tokenEstimateStartedAt = Date.now();
22
- estimatedInputTokens = requestPayload
23
- ? (estimatorOverrideForTests
24
- ? estimatorOverrideForTests(requestPayload)
25
- : estimateInputTokensFromRequest(requestPayload))
26
- : null;
27
- options.client.debugLog("Protect token estimation completed", {
28
- provider: "anthropic",
29
- model: requestedModel,
30
- latency_ms: Date.now() - tokenEstimateStartedAt,
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: extractResponseModel(response) ?? requestedModel,
54
- environment: options.environment ?? options.client.environment,
55
- request: {
56
- endpoint: options.endpoint,
57
- feature: options.feature,
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
- request: {
77
- endpoint: options.endpoint,
78
- feature: options.feature,
79
- token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
80
- input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
81
- protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
82
- protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
83
- },
84
- response: {
85
- latency_ms: Date.now() - startedAt,
86
- error_type: extractErrorType(error),
87
- http_status: extractHttpStatus(error),
88
- },
89
- }));
90
- throw error;
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 startedAt = Date.now();
17
- const requestedModel = extractRequestedModel(googleModel);
18
- validateProviderModel("google", requestedModel);
19
- const requestPayload = extractRequestPayload(args, requestedModel);
20
- let estimatedInputTokens = null;
21
- const tokenEstimateStartedAt = Date.now();
22
- estimatedInputTokens = requestPayload
23
- ? (estimatorOverrideForTests
24
- ? estimatorOverrideForTests(requestPayload)
25
- : estimateInputTokensFromRequest(requestPayload))
26
- : null;
27
- options.client.debugLog("Protect token estimation completed", {
28
- provider: "google",
29
- model: requestedModel,
30
- latency_ms: Date.now() - tokenEstimateStartedAt,
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
- environment: options.environment ?? options.client.environment,
55
- request: {
56
- endpoint: options.endpoint,
57
- feature: options.feature,
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
- request: {
77
- endpoint: options.endpoint,
78
- feature: options.feature,
79
- token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
80
- input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
81
- protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
82
- protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
83
- },
84
- response: {
85
- latency_ms: Date.now() - startedAt,
86
- error_type: extractErrorType(error),
87
- http_status: extractHttpStatus(error),
88
- },
89
- }));
90
- throw error;
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 startedAt = Date.now();
17
- const model = extractRequestedModel(args);
18
- validateProviderModel("openai", model);
19
- const requestPayload = extractRequestPayload(args);
20
- const tokenEstimateStartedAt = Date.now();
21
- const estimatedInputTokens = requestPayload
22
- ? (estimatorOverrideForTests
23
- ? estimatorOverrideForTests(requestPayload)
24
- : estimateInputTokensFromRequest(requestPayload))
25
- : null;
26
- options.client.debugLog("Protect token estimation completed", {
27
- provider: "openai",
28
- model,
29
- latency_ms: Date.now() - tokenEstimateStartedAt,
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: extractResponseModel(response) ?? model,
55
- environment: options.environment ?? options.client.environment,
56
- request: {
57
- endpoint: options.endpoint,
58
- feature: options.feature,
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
- request: {
77
- endpoint: options.endpoint,
78
- feature: options.feature,
79
- token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
80
- protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
81
- protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
82
- },
83
- response: {
84
- latency_ms: Date.now() - startedAt,
85
- error_type: extractErrorType(error),
86
- http_status: extractHttpStatus(error),
87
- },
88
- }));
89
- throw error;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rheonic/sdk",
3
- "version": "0.1.0-beta.8",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "Node.js SDK for Rheonic observability and protect preflight enforcement.",
5
5
  "author": "Rheonic <founder@rheonic.dev>",
6
6
  "license": "MIT",