@rheonic/sdk 0.1.0-beta.10 → 0.1.0-beta.12

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,9 +4,13 @@ All notable changes to `@rheonic/sdk` will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Changed
8
+ - Google provider instrumentation and examples now align with the current official Google Gen AI JavaScript SDK shape: `@google/genai` with `ai.models.generateContent({...})`.
9
+
7
10
  ### Fixed
8
11
  - SDK debug logs now always emit non-empty `trace_id` and `span_id`, including warmup, token-estimation, and protect preflight paths.
9
12
  - Provider instrumentation now keeps the full protected call lifecycle under one trace so SDK debug logs correlate cleanly with backend requests.
13
+ - Quickstart and README examples now use the published `@rheonic/sdk` package name consistently and show the current Anthropic and Google instrumentation patterns.
10
14
 
11
15
  ## 0.1.0-beta.7
12
16
 
package/README.md CHANGED
@@ -69,14 +69,18 @@ Anthropic:
69
69
 
70
70
  ```ts
71
71
  import Anthropic from "@anthropic-ai/sdk";
72
- import { createClient, RHEONICBlockedError } from "@rheonic/sdk";
72
+ import { createClient, instrumentAnthropic, RHEONICBlockedError } from "@rheonic/sdk";
73
73
 
74
74
  const rheonic = createClient({
75
75
  baseUrl: process.env.RHEONIC_BASE_URL!,
76
76
  ingestKey: process.env.RHEONIC_INGEST_KEY!,
77
77
  });
78
78
 
79
- const anthropic = rheonic.instrumentAnthropic(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }));
79
+ const anthropic = instrumentAnthropic(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }), {
80
+ client: rheonic,
81
+ endpoint: "/v1/messages",
82
+ feature: "assistant",
83
+ });
80
84
 
81
85
  try {
82
86
  await anthropic.messages.create({
@@ -100,19 +104,28 @@ try {
100
104
  Google:
101
105
 
102
106
  ```ts
103
- import { GoogleGenerativeAI } from "@google/generative-ai";
104
- import { createClient, RHEONICBlockedError } from "@rheonic/sdk";
107
+ import { GoogleGenAI } from "@google/genai";
108
+ import { createClient, instrumentGoogle, RHEONICBlockedError } from "@rheonic/sdk";
105
109
 
106
110
  const rheonic = createClient({
107
111
  baseUrl: process.env.RHEONIC_BASE_URL!,
108
112
  ingestKey: process.env.RHEONIC_INGEST_KEY!,
109
113
  });
110
114
 
111
- const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
112
- const model = rheonic.instrumentGoogle(genAI.getGenerativeModel({ model: "gemini-1.5-pro" }));
115
+ const ai = instrumentGoogle(new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY! }), {
116
+ client: rheonic,
117
+ endpoint: "/v1beta/models/generateContent",
118
+ feature: "assistant",
119
+ });
113
120
 
114
121
  try {
115
- await model.generateContent("hello");
122
+ await ai.models.generateContent({
123
+ model: "gemini-1.5-pro",
124
+ contents: "hello",
125
+ config: {
126
+ maxOutputTokens: 256,
127
+ },
128
+ });
116
129
  } catch (error) {
117
130
  if (error instanceof RHEONICBlockedError) {
118
131
  console.log(JSON.stringify({
@@ -1,6 +1,7 @@
1
1
  export interface EventRequest {
2
2
  endpoint?: string;
3
3
  feature?: string;
4
+ request_fingerprint?: string;
4
5
  input_tokens?: number;
5
6
  input_tokens_estimate?: number;
6
7
  token_explosion_tokens?: number;
@@ -17,14 +18,16 @@ export interface EventResponse {
17
18
  export interface EventPayload {
18
19
  ts: string;
19
20
  provider: string;
20
- model: string | null;
21
+ requested_model: string | null;
22
+ resolved_model: string | null;
21
23
  environment: string;
22
24
  request: EventRequest;
23
25
  response: EventResponse;
24
26
  }
25
27
  export interface BuildEventInput {
26
28
  provider: string;
27
- model?: string | null;
29
+ requested_model?: string | null;
30
+ resolved_model?: string | null;
28
31
  environment?: string;
29
32
  ts?: string;
30
33
  request?: EventRequest;
@@ -2,7 +2,8 @@ export function buildEvent(input) {
2
2
  return {
3
3
  ts: input.ts ?? new Date().toISOString(),
4
4
  provider: input.provider,
5
- model: input.model ?? null,
5
+ requested_model: input.requested_model ?? null,
6
+ resolved_model: input.resolved_model ?? null,
6
7
  environment: input.environment ?? "dev",
7
8
  request: input.request ?? {},
8
9
  response: input.response ?? {},
@@ -2,7 +2,7 @@ export type ProtectDecision = "allow" | "clamp" | "block";
2
2
  export type ProtectFailMode = "open" | "closed";
3
3
  export interface ProtectContext {
4
4
  provider: string;
5
- model?: string | null;
5
+ requested_model?: string | null;
6
6
  feature?: string;
7
7
  max_output_tokens?: number;
8
8
  input_tokens_estimate?: number;
@@ -90,7 +90,7 @@ export class ProtectEngine {
90
90
  status_code: response.status,
91
91
  latency_ms: Date.now() - startedAt,
92
92
  });
93
- void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
93
+ void this.reportDecisionUnavailable(context.provider, typeof context.requested_model === "string" ? context.requested_model : undefined, requestId, traceId);
94
94
  return this.fallbackEvaluation(traceId, requestId);
95
95
  }
96
96
  const parsed = (await response.json());
@@ -140,7 +140,7 @@ export class ProtectEngine {
140
140
  latency_ms: Date.now() - startedAt,
141
141
  timeout_ms: timeoutMs,
142
142
  });
143
- void this.reportDecisionTimeout(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
143
+ void this.reportDecisionTimeout(context.provider, typeof context.requested_model === "string" ? context.requested_model : undefined, requestId, traceId);
144
144
  }
145
145
  else {
146
146
  this.debugLog?.("Protect preflight failed", {
@@ -148,7 +148,7 @@ export class ProtectEngine {
148
148
  latency_ms: Date.now() - startedAt,
149
149
  error_type: extractErrorType(error),
150
150
  });
151
- void this.reportDecisionUnavailable(context.provider, typeof context.model === "string" ? context.model : undefined, requestId, traceId);
151
+ void this.reportDecisionUnavailable(context.provider, typeof context.requested_model === "string" ? context.requested_model : undefined, requestId, traceId);
152
152
  }
153
153
  return this.fallbackEvaluation(traceId, requestId);
154
154
  }
@@ -189,7 +189,7 @@ export class ProtectEngine {
189
189
  // Best effort only; keep local defaults if bootstrap fails.
190
190
  }
191
191
  }
192
- async reportDecisionTimeout(provider, model, requestId, traceId) {
192
+ async reportDecisionTimeout(provider, requestedModel, requestId, traceId) {
193
193
  try {
194
194
  await requestJson(`${this.baseUrl}/api/v1/protect/decision-timeout`, {
195
195
  method: "POST",
@@ -200,14 +200,19 @@ export class ProtectEngine {
200
200
  "X-Span-ID": generateSpanId(),
201
201
  "X-Rheonic-Protect-Request-Id": requestId,
202
202
  },
203
- body: JSON.stringify({ environment: this.environment, provider, model, request_id: requestId }),
203
+ body: JSON.stringify({
204
+ environment: this.environment,
205
+ provider,
206
+ requested_model: requestedModel,
207
+ request_id: requestId,
208
+ }),
204
209
  });
205
210
  }
206
211
  catch {
207
212
  // Swallow timeout reporting errors; protect evaluation must never throw here.
208
213
  }
209
214
  }
210
- async reportDecisionUnavailable(provider, model, requestId, traceId) {
215
+ async reportDecisionUnavailable(provider, requestedModel, requestId, traceId) {
211
216
  try {
212
217
  await requestJson(`${this.baseUrl}/api/v1/protect/decision-unavailable`, {
213
218
  method: "POST",
@@ -218,7 +223,12 @@ export class ProtectEngine {
218
223
  "X-Span-ID": generateSpanId(),
219
224
  "X-Rheonic-Protect-Request-Id": requestId,
220
225
  },
221
- body: JSON.stringify({ environment: this.environment, provider, model, request_id: requestId }),
226
+ body: JSON.stringify({
227
+ environment: this.environment,
228
+ provider,
229
+ requested_model: requestedModel,
230
+ request_id: requestId,
231
+ }),
222
232
  });
223
233
  }
224
234
  catch {
@@ -36,7 +36,7 @@ export function instrumentAnthropic(anthropicClient, options) {
36
36
  });
37
37
  const protectPayload = {
38
38
  provider: "anthropic",
39
- model: requestedModel,
39
+ requested_model: requestedModel,
40
40
  environment: options.environment ?? options.client.environment,
41
41
  feature: options.feature,
42
42
  max_output_tokens: extractMaxOutputTokens(args),
@@ -56,7 +56,8 @@ export function instrumentAnthropic(anthropicClient, options) {
56
56
  const response = await originalCreate(...callArgs);
57
57
  await options.client.captureEventAndFlush(buildEvent({
58
58
  provider: "anthropic",
59
- model: extractResponseModel(response) ?? requestedModel,
59
+ requested_model: requestedModel,
60
+ resolved_model: extractResponseModel(response),
60
61
  environment: options.environment ?? options.client.environment,
61
62
  request: {
62
63
  endpoint: options.endpoint,
@@ -77,7 +78,8 @@ export function instrumentAnthropic(anthropicClient, options) {
77
78
  catch (error) {
78
79
  await options.client.captureEventAndFlush(buildEvent({
79
80
  provider: "anthropic",
80
- model: requestedModel,
81
+ requested_model: requestedModel,
82
+ resolved_model: null,
81
83
  environment: options.environment ?? options.client.environment,
82
84
  request: {
83
85
  endpoint: options.endpoint,
@@ -8,17 +8,17 @@ export function __setInputTokenEstimatorForTests(estimator) {
8
8
  estimatorOverrideForTests = estimator;
9
9
  }
10
10
  export function instrumentGoogle(googleModel, options) {
11
- const targetGenerate = googleModel?.generateContent;
12
- if (typeof targetGenerate !== "function") {
11
+ const targetGenerate = resolveGenerateTarget(googleModel);
12
+ if (!targetGenerate) {
13
13
  return googleModel;
14
14
  }
15
- const originalGenerate = targetGenerate.bind(googleModel);
16
- googleModel.generateContent = async (...args) => {
15
+ const originalGenerate = targetGenerate.fn.bind(targetGenerate.owner);
16
+ targetGenerate.owner[targetGenerate.key] = async (...args) => {
17
17
  const traceId = generateTraceId();
18
18
  const spanId = generateSpanId();
19
19
  return bindTraceContext(traceId, spanId, async () => {
20
20
  const startedAt = Date.now();
21
- const requestedModel = extractRequestedModel(googleModel);
21
+ const requestedModel = extractRequestedModel(googleModel, args);
22
22
  validateProviderModel("google", requestedModel);
23
23
  const requestPayload = extractRequestPayload(args, requestedModel);
24
24
  let estimatedInputTokens = null;
@@ -36,7 +36,7 @@ export function instrumentGoogle(googleModel, options) {
36
36
  });
37
37
  const protectPayload = {
38
38
  provider: "google",
39
- model: requestedModel,
39
+ requested_model: requestedModel,
40
40
  environment: options.environment ?? options.client.environment,
41
41
  feature: options.feature,
42
42
  max_output_tokens: extractMaxOutputTokens(args),
@@ -56,7 +56,8 @@ export function instrumentGoogle(googleModel, options) {
56
56
  const response = await originalGenerate(...callArgs);
57
57
  await options.client.captureEventAndFlush(buildEvent({
58
58
  provider: "google",
59
- model: requestedModel,
59
+ requested_model: requestedModel,
60
+ resolved_model: extractResponseModel(response),
60
61
  environment: options.environment ?? options.client.environment,
61
62
  request: {
62
63
  endpoint: options.endpoint,
@@ -77,7 +78,8 @@ export function instrumentGoogle(googleModel, options) {
77
78
  catch (error) {
78
79
  await options.client.captureEventAndFlush(buildEvent({
79
80
  provider: "google",
80
- model: requestedModel,
81
+ requested_model: requestedModel,
82
+ resolved_model: null,
81
83
  environment: options.environment ?? options.client.environment,
82
84
  request: {
83
85
  endpoint: options.endpoint,
@@ -99,6 +101,26 @@ export function instrumentGoogle(googleModel, options) {
99
101
  };
100
102
  return googleModel;
101
103
  }
104
+ function resolveGenerateTarget(googleModel) {
105
+ const directGenerate = googleModel?.generateContent;
106
+ if (typeof directGenerate === "function") {
107
+ return {
108
+ owner: googleModel,
109
+ fn: directGenerate,
110
+ key: "generateContent",
111
+ };
112
+ }
113
+ const models = googleModel?.models;
114
+ const nestedGenerate = models?.generateContent;
115
+ if (models && typeof nestedGenerate === "function") {
116
+ return {
117
+ owner: models,
118
+ fn: nestedGenerate,
119
+ key: "generateContent",
120
+ };
121
+ }
122
+ return null;
123
+ }
102
124
  function extractRequestPayload(args, model) {
103
125
  const firstArg = args[0];
104
126
  if (typeof firstArg === "string") {
@@ -109,8 +131,13 @@ function extractRequestPayload(args, model) {
109
131
  }
110
132
  return null;
111
133
  }
112
- function extractRequestedModel(googleModel) {
134
+ function extractRequestedModel(googleModel, args) {
113
135
  if (!googleModel || typeof googleModel !== "object") {
136
+ const firstArg = args[0];
137
+ if (firstArg && typeof firstArg === "object" && "model" in firstArg) {
138
+ const payloadModel = firstArg.model;
139
+ return typeof payloadModel === "string" ? payloadModel : null;
140
+ }
114
141
  return null;
115
142
  }
116
143
  const withModel = googleModel;
@@ -120,16 +147,35 @@ function extractRequestedModel(googleModel) {
120
147
  if (typeof withModel.modelName === "string") {
121
148
  return withModel.modelName;
122
149
  }
150
+ const firstArg = args[0];
151
+ if (firstArg && typeof firstArg === "object" && "model" in firstArg) {
152
+ const payloadModel = firstArg.model;
153
+ return typeof payloadModel === "string" ? payloadModel : null;
154
+ }
123
155
  return null;
124
156
  }
125
157
  function extractMaxOutputTokens(args) {
126
158
  const firstArg = args[0];
127
159
  if (!firstArg || typeof firstArg !== "object") {
128
- return undefined;
160
+ const secondArg = args[1];
161
+ if (!secondArg || typeof secondArg !== "object") {
162
+ return undefined;
163
+ }
164
+ const options = secondArg;
165
+ const optionsGenerationMax = options.generationConfig?.maxOutputTokens;
166
+ if (typeof optionsGenerationMax === "number") {
167
+ return optionsGenerationMax;
168
+ }
169
+ const optionsConfigMax = options.config?.maxOutputTokens;
170
+ return typeof optionsConfigMax === "number" ? optionsConfigMax : undefined;
129
171
  }
130
172
  const payload = firstArg;
131
- const maxOutput = payload.generationConfig?.maxOutputTokens;
132
- return typeof maxOutput === "number" ? maxOutput : undefined;
173
+ const generationMax = payload.generationConfig?.maxOutputTokens;
174
+ if (typeof generationMax === "number") {
175
+ return generationMax;
176
+ }
177
+ const configMax = payload.config?.maxOutputTokens;
178
+ return typeof configMax === "number" ? configMax : undefined;
133
179
  }
134
180
  function extractTotalTokens(response) {
135
181
  if (!response || typeof response !== "object") {
@@ -149,6 +195,23 @@ function extractTotalTokens(response) {
149
195
  const total = prompt + candidates;
150
196
  return total > 0 ? total : undefined;
151
197
  }
198
+ function extractResponseModel(response) {
199
+ if (!response || typeof response !== "object") {
200
+ return null;
201
+ }
202
+ const topLevelModel = response.model;
203
+ if (typeof topLevelModel === "string" && topLevelModel.trim()) {
204
+ return topLevelModel;
205
+ }
206
+ const nestedResponse = response.response;
207
+ if (typeof nestedResponse?.modelVersion === "string" && nestedResponse.modelVersion.trim()) {
208
+ return nestedResponse.modelVersion;
209
+ }
210
+ if (typeof nestedResponse?.model === "string" && nestedResponse.model.trim()) {
211
+ return nestedResponse.model;
212
+ }
213
+ return null;
214
+ }
152
215
  function extractErrorType(error) {
153
216
  if (error && typeof error === "object" && "name" in error) {
154
217
  const name = error.name;
@@ -189,11 +252,13 @@ function maybeApplyGoogleClamp(args, decision) {
189
252
  const firstArg = nextArgs[0];
190
253
  if (firstArg && typeof firstArg === "object") {
191
254
  const payload = { ...firstArg };
192
- const existingConfig = payload.generationConfig && typeof payload.generationConfig === "object"
193
- ? payload.generationConfig
255
+ const useModernConfig = "config" in payload || "contents" in payload || "model" in payload;
256
+ const configKey = useModernConfig ? "config" : "generationConfig";
257
+ const existingConfig = payload[configKey] && typeof payload[configKey] === "object"
258
+ ? payload[configKey]
194
259
  : {};
195
260
  const existingMax = existingConfig.maxOutputTokens;
196
- payload.generationConfig = {
261
+ payload[configKey] = {
197
262
  ...existingConfig,
198
263
  maxOutputTokens: typeof existingMax === "number" ? Math.min(existingMax, recommended) : recommended,
199
264
  };
@@ -201,14 +266,15 @@ function maybeApplyGoogleClamp(args, decision) {
201
266
  return nextArgs;
202
267
  }
203
268
  const secondArg = nextArgs[1];
204
- const existingConfig = secondArg && typeof secondArg === "object" ? { ...secondArg } : {};
205
- const generationConfig = existingConfig.generationConfig && typeof existingConfig.generationConfig === "object"
206
- ? { ...existingConfig.generationConfig }
269
+ const existingOptions = secondArg && typeof secondArg === "object" ? { ...secondArg } : {};
270
+ const configKey = existingOptions.config && typeof existingOptions.config === "object" ? "config" : "generationConfig";
271
+ const generationConfig = existingOptions[configKey] && typeof existingOptions[configKey] === "object"
272
+ ? { ...existingOptions[configKey] }
207
273
  : {};
208
274
  const existingMax = generationConfig.maxOutputTokens;
209
275
  generationConfig.maxOutputTokens = typeof existingMax === "number" ? Math.min(existingMax, recommended) : recommended;
210
- existingConfig.generationConfig = generationConfig;
211
- nextArgs[1] = existingConfig;
276
+ existingOptions[configKey] = generationConfig;
277
+ nextArgs[1] = existingOptions;
212
278
  return nextArgs;
213
279
  }
214
280
  function markClampAppliedIfChanged(decision, originalMaxTokens, appliedMaxTokens) {
@@ -35,7 +35,7 @@ export function instrumentOpenAI(openaiClient, options) {
35
35
  });
36
36
  const protectPayload = {
37
37
  provider: "openai",
38
- model,
38
+ requested_model: model,
39
39
  environment: options.environment ?? options.client.environment,
40
40
  feature: options.feature,
41
41
  max_output_tokens: extractMaxOutputTokens(args),
@@ -57,7 +57,8 @@ export function instrumentOpenAI(openaiClient, options) {
57
57
  const response = await originalCreate(...callArgs);
58
58
  await options.client.captureEventAndFlush(buildEvent({
59
59
  provider: "openai",
60
- model: extractResponseModel(response) ?? model,
60
+ requested_model: model,
61
+ resolved_model: extractResponseModel(response),
61
62
  environment: options.environment ?? options.client.environment,
62
63
  request: {
63
64
  endpoint: options.endpoint,
@@ -77,7 +78,8 @@ export function instrumentOpenAI(openaiClient, options) {
77
78
  catch (error) {
78
79
  await options.client.captureEventAndFlush(buildEvent({
79
80
  provider: "openai",
80
- model,
81
+ requested_model: model,
82
+ resolved_model: null,
81
83
  environment: options.environment ?? options.client.environment,
82
84
  request: {
83
85
  endpoint: options.endpoint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rheonic/sdk",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.12",
4
4
  "description": "Node.js SDK for Rheonic observability and protect preflight enforcement.",
5
5
  "author": "Rheonic <founder@rheonic.dev>",
6
6
  "license": "MIT",