@rheonic/sdk 0.1.0-beta.13 → 0.1.0-beta.15

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/dist/client.d.ts CHANGED
@@ -1,8 +1,8 @@
1
+ import { type BuildEventInput, type EventPayload } from "./eventBuilder.js";
1
2
  import { type ProtectContext, type ProtectEvaluation, type ProtectFailMode } from "./protectEngine.js";
2
3
  import { type AnthropicInstrumentationOptions } from "./providers/anthropicAdapter.js";
3
4
  import { type GoogleInstrumentationOptions } from "./providers/googleAdapter.js";
4
5
  import { type OpenAIInstrumentationOptions } from "./providers/openaiAdapter.js";
5
- import type { EventPayload } from "./eventBuilder.js";
6
6
  export type OverflowPolicy = "drop_oldest" | "drop_newest";
7
7
  export interface ClientStats {
8
8
  queued: number;
@@ -41,8 +41,8 @@ export declare class Client {
41
41
  private warmupPromise;
42
42
  private warmupCompleted;
43
43
  constructor(config: ClientConfig);
44
- captureEvent(event: EventPayload): Promise<void>;
45
- captureEventAndFlush(event: EventPayload): Promise<void>;
44
+ captureEvent(event: EventPayload | BuildEventInput): Promise<void>;
45
+ captureEventAndFlush(event: EventPayload | BuildEventInput): Promise<void>;
46
46
  getStats(): ClientStats;
47
47
  flush(): Promise<void>;
48
48
  evaluateProtectDecision(context: ProtectContext): Promise<ProtectEvaluation>;
package/dist/client.js CHANGED
@@ -1,13 +1,66 @@
1
1
  import { sdkNodeConfig } from "./config.js";
2
+ import { normalizeEventPayload } from "./eventBuilder.js";
2
3
  import { requestJson } from "./httpTransport.js";
3
4
  import { bindTraceContext, emitLog, generateSpanId, generateTraceId, getSpanId, getTraceId } from "./logger.js";
4
5
  import { ProtectEngine } from "./protectEngine.js";
6
+ import { RHEONICValidationError } from "./providerModelValidation.js";
5
7
  import { instrumentAnthropic as instrumentAnthropicProvider } from "./providers/anthropicAdapter.js";
6
8
  import { instrumentGoogle as instrumentGoogleProvider } from "./providers/googleAdapter.js";
7
9
  import { instrumentOpenAI as instrumentOpenAIProvider } from "./providers/openaiAdapter.js";
8
10
  import { prewarmTokenEstimator } from "./tokenEstimator.js";
9
11
  const CLIENT_REGISTRY = new Set();
10
12
  let EXIT_HOOKS_REGISTERED = false;
13
+ const CLIENT_CONFIG_KEYS = new Set([
14
+ "baseUrl",
15
+ "ingestKey",
16
+ "environment",
17
+ "flushIntervalMs",
18
+ "maxQueueSize",
19
+ "overflowPolicy",
20
+ "requestTimeoutMs",
21
+ "protectFailMode",
22
+ "debug",
23
+ ]);
24
+ function failClientValidation(message) {
25
+ throw new RHEONICValidationError(message, "client", "", []);
26
+ }
27
+ function validateClientConfig(config) {
28
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
29
+ failClientValidation("RHEONIC: createClient config must be an object.");
30
+ }
31
+ for (const key of Object.keys(config)) {
32
+ if (!CLIENT_CONFIG_KEYS.has(key)) {
33
+ failClientValidation(`RHEONIC: unexpected client config property: ${key}`);
34
+ }
35
+ }
36
+ if (typeof config.ingestKey !== "string" || config.ingestKey.trim().length === 0) {
37
+ failClientValidation("RHEONIC: ingestKey must be a non-empty string.");
38
+ }
39
+ if (config.baseUrl !== undefined && (typeof config.baseUrl !== "string" || config.baseUrl.trim().length === 0)) {
40
+ failClientValidation("RHEONIC: baseUrl must be a non-empty string.");
41
+ }
42
+ if (config.environment !== undefined && (typeof config.environment !== "string" || config.environment.trim().length === 0)) {
43
+ failClientValidation("RHEONIC: environment must be a non-empty string.");
44
+ }
45
+ if (config.flushIntervalMs !== undefined && (!Number.isFinite(config.flushIntervalMs) || config.flushIntervalMs <= 0)) {
46
+ failClientValidation("RHEONIC: flushIntervalMs must be a positive number.");
47
+ }
48
+ if (config.maxQueueSize !== undefined && (!Number.isInteger(config.maxQueueSize) || config.maxQueueSize < 1)) {
49
+ failClientValidation("RHEONIC: maxQueueSize must be a positive integer.");
50
+ }
51
+ if (config.overflowPolicy !== undefined && !["drop_oldest", "drop_newest"].includes(config.overflowPolicy)) {
52
+ failClientValidation("RHEONIC: overflowPolicy must be 'drop_oldest' or 'drop_newest'.");
53
+ }
54
+ if (config.requestTimeoutMs !== undefined && (!Number.isFinite(config.requestTimeoutMs) || config.requestTimeoutMs <= 0)) {
55
+ failClientValidation("RHEONIC: requestTimeoutMs must be a positive number.");
56
+ }
57
+ if (config.protectFailMode !== undefined && !["open", "closed"].includes(config.protectFailMode)) {
58
+ failClientValidation("RHEONIC: protectFailMode must be 'open' or 'closed'.");
59
+ }
60
+ if (config.debug !== undefined && typeof config.debug !== "boolean") {
61
+ failClientValidation("RHEONIC: debug must be a boolean.");
62
+ }
63
+ }
11
64
  export class Client {
12
65
  baseUrl;
13
66
  ingestKey;
@@ -28,6 +81,7 @@ export class Client {
28
81
  warmupPromise = null;
29
82
  warmupCompleted = false;
30
83
  constructor(config) {
84
+ validateClientConfig(config);
31
85
  this.baseUrl = config.baseUrl ?? process.env.RHEONIC_BASE_URL ?? sdkNodeConfig.defaultBaseUrl;
32
86
  this.ingestKey = config.ingestKey;
33
87
  this.environment =
@@ -75,28 +129,24 @@ export class Client {
75
129
  });
76
130
  }
77
131
  async captureEvent(event) {
78
- try {
79
- if (this.queue.length >= this.maxQueueSize) {
80
- if (this.overflowPolicy === "drop_oldest") {
81
- this.queue.shift();
82
- this.dropped += 1;
83
- }
84
- else {
85
- this.dropped += 1;
86
- return;
87
- }
132
+ const normalizedEvent = normalizeEventPayload(event);
133
+ if (this.queue.length >= this.maxQueueSize) {
134
+ if (this.overflowPolicy === "drop_oldest") {
135
+ this.queue.shift();
136
+ this.dropped += 1;
88
137
  }
89
- if (this.isClosed) {
138
+ else {
139
+ this.dropped += 1;
90
140
  return;
91
141
  }
92
- this.queue.push({
93
- ...event,
94
- environment: event.environment || this.environment,
95
- });
96
142
  }
97
- catch {
143
+ if (this.isClosed) {
98
144
  return;
99
145
  }
146
+ this.queue.push({
147
+ ...normalizedEvent,
148
+ environment: normalizedEvent.environment || this.environment,
149
+ });
100
150
  }
101
151
  async captureEventAndFlush(event) {
102
152
  await this.captureEvent(event);
@@ -12,6 +12,7 @@ export interface EventRequest {
12
12
  export interface EventResponse {
13
13
  http_status?: number;
14
14
  latency_ms?: number;
15
+ output_tokens?: number;
15
16
  total_tokens?: number;
16
17
  error_type?: string;
17
18
  error_message?: string;
@@ -22,18 +23,32 @@ export interface EventPayload {
22
23
  requested_model: string | null;
23
24
  resolved_model: string | null;
24
25
  environment: string;
26
+ status?: string;
25
27
  request: EventRequest;
26
28
  response: EventResponse;
27
29
  }
28
30
  export interface BuildEventInput {
31
+ provider: string;
32
+ model?: string | null;
33
+ environment?: string;
34
+ ts?: string;
35
+ status?: string;
36
+ request?: EventRequest;
37
+ response?: EventResponse;
38
+ }
39
+ export interface InternalEventInput {
29
40
  provider: string;
30
41
  requested_model?: string | null;
31
42
  resolved_model?: string | null;
32
43
  environment?: string;
33
44
  ts?: string;
45
+ status?: string;
34
46
  request?: EventRequest;
35
47
  response?: EventResponse;
36
48
  }
49
+ export declare function validateEventPayload(input: EventPayload): EventPayload;
50
+ export declare function normalizeEventPayload(input: EventPayload | BuildEventInput): EventPayload;
51
+ export declare function buildInternalEvent(input: InternalEventInput): EventPayload;
37
52
  export declare function buildEvent(input: BuildEventInput): EventPayload;
38
53
  export declare class EventBuilder {
39
54
  build(payload: BuildEventInput): EventPayload;
@@ -1,14 +1,174 @@
1
- export function buildEvent(input) {
1
+ import { RHEONICValidationError } from "./providerModelValidation.js";
2
+ const PUBLIC_EVENT_KEYS = new Set(["provider", "model", "environment", "ts", "status", "request", "response"]);
3
+ const INTERNAL_EVENT_KEYS = new Set([
4
+ "provider",
5
+ "requested_model",
6
+ "resolved_model",
7
+ "environment",
8
+ "ts",
9
+ "status",
10
+ "request",
11
+ "response",
12
+ ]);
13
+ const REQUEST_KEYS = new Set([
14
+ "endpoint",
15
+ "feature",
16
+ "request_fingerprint",
17
+ "input_tokens",
18
+ "input_tokens_estimate",
19
+ "token_explosion_tokens",
20
+ "max_output_tokens",
21
+ "protect_decision",
22
+ "protect_reason",
23
+ ]);
24
+ const RESPONSE_KEYS = new Set(["http_status", "latency_ms", "output_tokens", "total_tokens", "error_type", "error_message"]);
25
+ function failValidation(message) {
26
+ throw new RHEONICValidationError(message, "event", "", []);
27
+ }
28
+ function assertPlainObject(value, label) {
29
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
30
+ failValidation(`RHEONIC: ${label} must be an object.`);
31
+ }
32
+ return value;
33
+ }
34
+ function assertNoExtraKeys(record, allowedKeys, label) {
35
+ for (const key of Object.keys(record)) {
36
+ if (!allowedKeys.has(key)) {
37
+ failValidation(`RHEONIC: unexpected ${label} property: ${key}`);
38
+ }
39
+ }
40
+ }
41
+ function assertOptionalString(value, label) {
42
+ if (value !== undefined && value !== null && typeof value !== "string") {
43
+ failValidation(`RHEONIC: ${label} must be a string.`);
44
+ }
45
+ }
46
+ function assertOptionalNumber(value, label) {
47
+ if (value !== undefined && value !== null && typeof value !== "number") {
48
+ failValidation(`RHEONIC: ${label} must be a number.`);
49
+ }
50
+ }
51
+ function validateRequestShape(request) {
52
+ const record = assertPlainObject(request, "event.request");
53
+ assertNoExtraKeys(record, REQUEST_KEYS, "event.request");
54
+ assertOptionalString(record.endpoint, "event.request.endpoint");
55
+ assertOptionalString(record.feature, "event.request.feature");
56
+ assertOptionalString(record.request_fingerprint, "event.request.request_fingerprint");
57
+ assertOptionalNumber(record.input_tokens, "event.request.input_tokens");
58
+ assertOptionalNumber(record.input_tokens_estimate, "event.request.input_tokens_estimate");
59
+ assertOptionalNumber(record.token_explosion_tokens, "event.request.token_explosion_tokens");
60
+ assertOptionalNumber(record.max_output_tokens, "event.request.max_output_tokens");
61
+ assertOptionalString(record.protect_decision, "event.request.protect_decision");
62
+ assertOptionalString(record.protect_reason, "event.request.protect_reason");
63
+ return record;
64
+ }
65
+ function validateResponseShape(response) {
66
+ const record = assertPlainObject(response, "event.response");
67
+ assertNoExtraKeys(record, RESPONSE_KEYS, "event.response");
68
+ assertOptionalNumber(record.http_status, "event.response.http_status");
69
+ assertOptionalNumber(record.latency_ms, "event.response.latency_ms");
70
+ assertOptionalNumber(record.output_tokens, "event.response.output_tokens");
71
+ assertOptionalNumber(record.total_tokens, "event.response.total_tokens");
72
+ assertOptionalString(record.error_type, "event.response.error_type");
73
+ assertOptionalString(record.error_message, "event.response.error_message");
74
+ return record;
75
+ }
76
+ function validateBuildEventInput(input) {
77
+ const record = assertPlainObject(input, "event");
78
+ assertNoExtraKeys(record, PUBLIC_EVENT_KEYS, "event");
79
+ if (typeof record.provider !== "string" || record.provider.trim().length === 0) {
80
+ failValidation("RHEONIC: event.provider must be a non-empty string.");
81
+ }
82
+ assertOptionalString(record.model, "event.model");
83
+ assertOptionalString(record.environment, "event.environment");
84
+ assertOptionalString(record.ts, "event.ts");
85
+ assertOptionalString(record.status, "event.status");
86
+ if (record.request !== undefined) {
87
+ validateRequestShape(record.request);
88
+ }
89
+ if (record.response !== undefined) {
90
+ validateResponseShape(record.response);
91
+ }
92
+ return input;
93
+ }
94
+ function validateInternalEventInput(input) {
95
+ const record = assertPlainObject(input, "event");
96
+ assertNoExtraKeys(record, INTERNAL_EVENT_KEYS, "event");
97
+ if (typeof record.provider !== "string" || record.provider.trim().length === 0) {
98
+ failValidation("RHEONIC: event.provider must be a non-empty string.");
99
+ }
100
+ assertOptionalString(record.requested_model, "event.requested_model");
101
+ assertOptionalString(record.resolved_model, "event.resolved_model");
102
+ assertOptionalString(record.environment, "event.environment");
103
+ assertOptionalString(record.ts, "event.ts");
104
+ assertOptionalString(record.status, "event.status");
105
+ if (record.request !== undefined) {
106
+ validateRequestShape(record.request);
107
+ }
108
+ if (record.response !== undefined) {
109
+ validateResponseShape(record.response);
110
+ }
111
+ return input;
112
+ }
113
+ export function validateEventPayload(input) {
114
+ const record = assertPlainObject(input, "event");
115
+ assertNoExtraKeys(record, INTERNAL_EVENT_KEYS, "event");
116
+ for (const requiredKey of ["ts", "provider", "requested_model", "resolved_model", "environment", "request", "response"]) {
117
+ if (!Object.prototype.hasOwnProperty.call(record, requiredKey)) {
118
+ failValidation(`RHEONIC: event.${requiredKey} is required.`);
119
+ }
120
+ }
121
+ if (typeof record.ts !== "string" || record.ts.trim().length === 0) {
122
+ failValidation("RHEONIC: event.ts must be a non-empty string.");
123
+ }
124
+ if (typeof record.provider !== "string" || record.provider.trim().length === 0) {
125
+ failValidation("RHEONIC: event.provider must be a non-empty string.");
126
+ }
127
+ if (typeof record.environment !== "string" || record.environment.trim().length === 0) {
128
+ failValidation("RHEONIC: event.environment must be a non-empty string.");
129
+ }
130
+ assertOptionalString(record.requested_model, "event.requested_model");
131
+ assertOptionalString(record.resolved_model, "event.resolved_model");
132
+ assertOptionalString(record.status, "event.status");
133
+ validateRequestShape(record.request);
134
+ validateResponseShape(record.response);
135
+ return input;
136
+ }
137
+ export function normalizeEventPayload(input) {
138
+ const record = assertPlainObject(input, "event");
139
+ const isInternal = Object.prototype.hasOwnProperty.call(record, "requested_model")
140
+ || Object.prototype.hasOwnProperty.call(record, "resolved_model");
141
+ if (isInternal) {
142
+ return validateEventPayload(input);
143
+ }
144
+ return buildEvent(input);
145
+ }
146
+ export function buildInternalEvent(input) {
147
+ validateInternalEventInput(input);
2
148
  return {
3
149
  ts: input.ts ?? new Date().toISOString(),
4
150
  provider: input.provider,
5
151
  requested_model: input.requested_model ?? null,
6
152
  resolved_model: input.resolved_model ?? null,
7
153
  environment: input.environment ?? "dev",
154
+ ...(input.status !== undefined ? { status: input.status } : {}),
8
155
  request: input.request ?? {},
9
156
  response: input.response ?? {},
10
157
  };
11
158
  }
159
+ export function buildEvent(input) {
160
+ validateBuildEventInput(input);
161
+ return buildInternalEvent({
162
+ provider: input.provider,
163
+ requested_model: input.model ?? null,
164
+ resolved_model: null,
165
+ environment: input.environment,
166
+ ts: input.ts,
167
+ status: input.status,
168
+ request: input.request,
169
+ response: input.response,
170
+ });
171
+ }
12
172
  export class EventBuilder {
13
173
  build(payload) {
14
174
  return buildEvent(payload);
@@ -3,6 +3,7 @@ export type ProtectFailMode = "open" | "closed";
3
3
  export interface ProtectContext {
4
4
  provider: string;
5
5
  requested_model?: string | null;
6
+ environment?: string;
6
7
  feature?: string;
7
8
  max_output_tokens?: number;
8
9
  input_tokens_estimate?: number;
@@ -71,6 +71,7 @@ export class ProtectEngine {
71
71
  timeout.unref?.();
72
72
  const startedAt = Date.now();
73
73
  try {
74
+ const requestBody = sanitizeProtectContext(context);
74
75
  const response = await requestJson(`${this.baseUrl}/api/v1/protect/decision`, {
75
76
  method: "POST",
76
77
  headers: {
@@ -80,7 +81,7 @@ export class ProtectEngine {
80
81
  "X-Span-ID": getSpanId(),
81
82
  "X-Rheonic-Protect-Request-Id": requestId,
82
83
  },
83
- body: JSON.stringify(context),
84
+ body: JSON.stringify(requestBody),
84
85
  signal: controller.signal,
85
86
  });
86
87
  clearTimeout(timeout);
@@ -236,6 +237,25 @@ export class ProtectEngine {
236
237
  }
237
238
  }
238
239
  }
240
+ function sanitizeProtectContext(context) {
241
+ const payload = { provider: context.provider };
242
+ if (context.requested_model != null) {
243
+ payload.requested_model = context.requested_model;
244
+ }
245
+ if (context.environment != null) {
246
+ payload.environment = context.environment;
247
+ }
248
+ if (context.feature != null) {
249
+ payload.feature = context.feature;
250
+ }
251
+ if (context.max_output_tokens != null) {
252
+ payload.max_output_tokens = context.max_output_tokens;
253
+ }
254
+ if (context.input_tokens_estimate != null) {
255
+ payload.input_tokens_estimate = context.input_tokens_estimate;
256
+ }
257
+ return payload;
258
+ }
239
259
  function parseClamp(value) {
240
260
  if (!value || typeof value !== "object") {
241
261
  return undefined;
@@ -1,7 +1,7 @@
1
- import { buildEvent } from "../eventBuilder.js";
2
- import { bindTraceContext, generateSpanId, generateTraceId } from "../logger.js";
1
+ import { buildInternalEvent } from "../eventBuilder.js";
2
+ import { bindTraceContext, generateSpanId, generateTraceId, getSpanId, getTraceId } from "../logger.js";
3
3
  import { RHEONICBlockedError } from "../protectEngine.js";
4
- import { validateProviderModel } from "../providerModelValidation.js";
4
+ import { RHEONICValidationError, validateProviderModel } from "../providerModelValidation.js";
5
5
  import { estimateInputTokensFromRequest } from "../tokenEstimator.js";
6
6
  let estimatorOverrideForTests = null;
7
7
  export function __setInputTokenEstimatorForTests(estimator) {
@@ -10,12 +10,12 @@ export function __setInputTokenEstimatorForTests(estimator) {
10
10
  export function instrumentAnthropic(anthropicClient, options) {
11
11
  const targetCreate = anthropicClient?.messages?.create;
12
12
  if (typeof targetCreate !== "function") {
13
- return anthropicClient;
13
+ throw new RHEONICValidationError("RHEONIC: instrumentAnthropic requires a client exposing messages.create.", "anthropic", "", ["anthropic"]);
14
14
  }
15
15
  const originalCreate = targetCreate.bind(anthropicClient.messages);
16
16
  anthropicClient.messages.create = async (...args) => {
17
- const traceId = generateTraceId();
18
- const spanId = generateSpanId();
17
+ const traceId = getTraceId() || generateTraceId();
18
+ const spanId = getSpanId() || generateSpanId();
19
19
  return bindTraceContext(traceId, spanId, async () => {
20
20
  const startedAt = Date.now();
21
21
  const requestPayload = extractRequestPayload(args);
@@ -54,10 +54,9 @@ export function instrumentAnthropic(anthropicClient, options) {
54
54
  markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
55
55
  try {
56
56
  const response = await originalCreate(...callArgs);
57
- await options.client.captureEventAndFlush(buildEvent({
57
+ const event = buildInternalEvent({
58
58
  provider: "anthropic",
59
59
  requested_model: requestedModel,
60
- resolved_model: extractResponseModel(response),
61
60
  environment: options.environment ?? options.client.environment,
62
61
  request: {
63
62
  endpoint: options.endpoint,
@@ -72,14 +71,15 @@ export function instrumentAnthropic(anthropicClient, options) {
72
71
  total_tokens: extractTotalTokens(response),
73
72
  http_status: 200,
74
73
  },
75
- }));
74
+ });
75
+ event.resolved_model = extractResponseModel(response);
76
+ await options.client.captureEventAndFlush(event);
76
77
  return response;
77
78
  }
78
79
  catch (error) {
79
- await options.client.captureEventAndFlush(buildEvent({
80
+ await options.client.captureEventAndFlush(buildInternalEvent({
80
81
  provider: "anthropic",
81
82
  requested_model: requestedModel,
82
- resolved_model: null,
83
83
  environment: options.environment ?? options.client.environment,
84
84
  request: {
85
85
  endpoint: options.endpoint,
@@ -1,7 +1,7 @@
1
- import { buildEvent } from "../eventBuilder.js";
2
- import { bindTraceContext, generateSpanId, generateTraceId } from "../logger.js";
1
+ import { buildInternalEvent } from "../eventBuilder.js";
2
+ import { bindTraceContext, generateSpanId, generateTraceId, getSpanId, getTraceId } from "../logger.js";
3
3
  import { RHEONICBlockedError } from "../protectEngine.js";
4
- import { validateProviderModel } from "../providerModelValidation.js";
4
+ import { RHEONICValidationError, validateProviderModel } from "../providerModelValidation.js";
5
5
  import { estimateInputTokensFromRequest } from "../tokenEstimator.js";
6
6
  let estimatorOverrideForTests = null;
7
7
  export function __setInputTokenEstimatorForTests(estimator) {
@@ -10,12 +10,12 @@ export function __setInputTokenEstimatorForTests(estimator) {
10
10
  export function instrumentGoogle(googleModel, options) {
11
11
  const targetGenerate = resolveGenerateTarget(googleModel);
12
12
  if (!targetGenerate) {
13
- return googleModel;
13
+ throw new RHEONICValidationError("RHEONIC: instrumentGoogle requires a client exposing generateContent or models.generateContent.", "google", "", ["google"]);
14
14
  }
15
15
  const originalGenerate = targetGenerate.fn.bind(targetGenerate.owner);
16
16
  targetGenerate.owner[targetGenerate.key] = async (...args) => {
17
- const traceId = generateTraceId();
18
- const spanId = generateSpanId();
17
+ const traceId = getTraceId() || generateTraceId();
18
+ const spanId = getSpanId() || generateSpanId();
19
19
  return bindTraceContext(traceId, spanId, async () => {
20
20
  const startedAt = Date.now();
21
21
  const requestedModel = extractRequestedModel(googleModel, args);
@@ -54,10 +54,9 @@ export function instrumentGoogle(googleModel, options) {
54
54
  markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
55
55
  try {
56
56
  const response = await originalGenerate(...callArgs);
57
- await options.client.captureEventAndFlush(buildEvent({
57
+ const event = buildInternalEvent({
58
58
  provider: "google",
59
59
  requested_model: requestedModel,
60
- resolved_model: extractResponseModel(response),
61
60
  environment: options.environment ?? options.client.environment,
62
61
  request: {
63
62
  endpoint: options.endpoint,
@@ -72,14 +71,15 @@ export function instrumentGoogle(googleModel, options) {
72
71
  total_tokens: extractTotalTokens(response),
73
72
  http_status: 200,
74
73
  },
75
- }));
74
+ });
75
+ event.resolved_model = extractResponseModel(response);
76
+ await options.client.captureEventAndFlush(event);
76
77
  return response;
77
78
  }
78
79
  catch (error) {
79
- await options.client.captureEventAndFlush(buildEvent({
80
+ await options.client.captureEventAndFlush(buildInternalEvent({
80
81
  provider: "google",
81
82
  requested_model: requestedModel,
82
- resolved_model: null,
83
83
  environment: options.environment ?? options.client.environment,
84
84
  request: {
85
85
  endpoint: options.endpoint,
@@ -1,7 +1,7 @@
1
- import { buildEvent } from "../eventBuilder.js";
2
- import { bindTraceContext, generateSpanId, generateTraceId } from "../logger.js";
1
+ import { buildInternalEvent } from "../eventBuilder.js";
2
+ import { bindTraceContext, generateSpanId, generateTraceId, getSpanId, getTraceId } from "../logger.js";
3
3
  import { RHEONICBlockedError } from "../protectEngine.js";
4
- import { validateProviderModel } from "../providerModelValidation.js";
4
+ import { RHEONICValidationError, validateProviderModel } from "../providerModelValidation.js";
5
5
  import { estimateInputTokensFromRequest } from "../tokenEstimator.js";
6
6
  let estimatorOverrideForTests = null;
7
7
  export function __setInputTokenEstimatorForTests(estimator) {
@@ -10,12 +10,12 @@ export function __setInputTokenEstimatorForTests(estimator) {
10
10
  export function instrumentOpenAI(openaiClient, options) {
11
11
  const targetCreate = openaiClient?.chat?.completions?.create;
12
12
  if (typeof targetCreate !== "function") {
13
- return openaiClient;
13
+ throw new RHEONICValidationError("RHEONIC: instrumentOpenAI requires a client exposing chat.completions.create.", "openai", "", ["openai"]);
14
14
  }
15
15
  const originalCreate = targetCreate.bind(openaiClient.chat.completions);
16
16
  openaiClient.chat.completions.create = async (...args) => {
17
- const traceId = generateTraceId();
18
- const spanId = generateSpanId();
17
+ const traceId = getTraceId() || generateTraceId();
18
+ const spanId = getSpanId() || generateSpanId();
19
19
  return bindTraceContext(traceId, spanId, async () => {
20
20
  const startedAt = Date.now();
21
21
  const model = extractRequestedModel(args);
@@ -55,15 +55,15 @@ export function instrumentOpenAI(openaiClient, options) {
55
55
  markClampAppliedIfChanged(protectDecision, extractMaxOutputTokens(args), extractMaxOutputTokens(callArgs));
56
56
  try {
57
57
  const response = await originalCreate(...callArgs);
58
- await options.client.captureEventAndFlush(buildEvent({
58
+ const event = buildInternalEvent({
59
59
  provider: "openai",
60
60
  requested_model: model,
61
- resolved_model: extractResponseModel(response),
62
61
  environment: options.environment ?? options.client.environment,
63
62
  request: {
64
63
  endpoint: options.endpoint,
65
64
  feature: options.feature,
66
65
  token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
66
+ input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
67
67
  protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
68
68
  protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
69
69
  },
@@ -72,19 +72,21 @@ export function instrumentOpenAI(openaiClient, options) {
72
72
  total_tokens: extractTotalTokens(response),
73
73
  http_status: 200,
74
74
  },
75
- }));
75
+ });
76
+ event.resolved_model = extractResponseModel(response);
77
+ await options.client.captureEventAndFlush(event);
76
78
  return response;
77
79
  }
78
80
  catch (error) {
79
- await options.client.captureEventAndFlush(buildEvent({
81
+ await options.client.captureEventAndFlush(buildInternalEvent({
80
82
  provider: "openai",
81
83
  requested_model: model,
82
- resolved_model: null,
83
84
  environment: options.environment ?? options.client.environment,
84
85
  request: {
85
86
  endpoint: options.endpoint,
86
87
  feature: options.feature,
87
88
  token_explosion_tokens: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
89
+ input_tokens_estimate: typeof estimatedInputTokens === "number" ? estimatedInputTokens : undefined,
88
90
  protect_decision: protectDecision.decision !== "allow" ? protectDecision.decision : undefined,
89
91
  protect_reason: protectDecision.decision !== "allow" ? protectDecision.reason : undefined,
90
92
  },
@@ -39,6 +39,17 @@ function extractTextForEstimation(request) {
39
39
  if (typeof request.prompt === "string") {
40
40
  return request.prompt;
41
41
  }
42
+ if (typeof request.contents === "string") {
43
+ return request.contents;
44
+ }
45
+ if (Array.isArray(request.contents)) {
46
+ try {
47
+ return JSON.stringify(request.contents);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
42
53
  return null;
43
54
  }
44
55
  function getEncoder(model) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rheonic/sdk",
3
- "version": "0.1.0-beta.13",
3
+ "version": "0.1.0-beta.15",
4
4
  "description": "Node.js SDK for Rheonic observability and protect preflight enforcement.",
5
5
  "author": "Rheonic <founder@rheonic.dev>",
6
6
  "license": "MIT",