@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 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.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(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
+ });
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 response = await bindTraceContext(generateTraceId(), generateSpanId(), async () => await requestJson(`${this.baseUrl}/health`, {
156
- method: "GET",
157
- headers: {
158
- "X-Trace-ID": getTraceId(),
159
- "X-Span-ID": getSpanId(),
160
- },
161
- }));
162
- 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
+ });
163
175
  }
164
176
  catch {
165
177
  this.debugLog("SDK connection warmup failed");
166
178
  }
167
179
  try {
168
- await this.protectEngine.bootstrap();
169
- 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
+ });
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: 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
- 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: 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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: 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
- 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
- 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.7",
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",