@traceai/google-genai-sdk 0.0.0-stage → 0.1.0
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/README.md +72 -2
- package/dist/capture.d.ts +44 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +414 -0
- package/dist/capture.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +543 -0
- package/dist/index.js.map +1 -0
- package/dist/promise.d.ts +6 -0
- package/dist/promise.d.ts.map +1 -0
- package/dist/promise.js +46 -0
- package/dist/promise.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +47 -3
- package/src/capture.ts +443 -0
- package/src/index.ts +668 -0
- package/src/promise.ts +52 -0
- package/src/version.ts +1 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import {
|
|
2
|
+
context,
|
|
3
|
+
trace,
|
|
4
|
+
diag,
|
|
5
|
+
SpanKind,
|
|
6
|
+
SpanStatusCode,
|
|
7
|
+
type Context,
|
|
8
|
+
type Span,
|
|
9
|
+
type TracerProvider,
|
|
10
|
+
} from "@opentelemetry/api";
|
|
11
|
+
import { isTracingSuppressed } from "@opentelemetry/core";
|
|
12
|
+
import {
|
|
13
|
+
FITracer,
|
|
14
|
+
generateTraceConfig,
|
|
15
|
+
getAttributesFromContext,
|
|
16
|
+
type TraceConfigOptions,
|
|
17
|
+
} from "@traceai/fi-core";
|
|
18
|
+
import { SemanticConventions as S } from "@traceai/fi-semantic-conventions";
|
|
19
|
+
import { Capture, record } from "./capture.js";
|
|
20
|
+
import { VERSION } from "./version.js";
|
|
21
|
+
import { wrapAPIPromise } from "./promise.js";
|
|
22
|
+
export { VERSION } from "./version.js";
|
|
23
|
+
|
|
24
|
+
export interface GoogleGenAITracingOptions {
|
|
25
|
+
tracerProvider?: TracerProvider;
|
|
26
|
+
traceConfig?: TraceConfigOptions;
|
|
27
|
+
/** Total captured content budget per call, in characters. Default 16,384; maximum 65,536. */
|
|
28
|
+
maxContentLength?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface GoogleGenAITracing {
|
|
31
|
+
/** Stops tracing new calls. In-flight calls finish normally. */
|
|
32
|
+
disable(): void;
|
|
33
|
+
enable(): void;
|
|
34
|
+
/** Stops new tracing; restores methods after in-flight calls drain. Does not shut down the provider. */
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Method = (this: unknown, ...args: unknown[]) => unknown;
|
|
39
|
+
const handles = new WeakMap<object, GoogleGenAITracing>();
|
|
40
|
+
const modelMethods = [
|
|
41
|
+
"generateContent",
|
|
42
|
+
"generateContentStream",
|
|
43
|
+
"embedContent",
|
|
44
|
+
"generateImages",
|
|
45
|
+
"editImage",
|
|
46
|
+
"generateVideos",
|
|
47
|
+
];
|
|
48
|
+
const interactionMethods = ["create", "get", "cancel", "delete"];
|
|
49
|
+
// Observability must never replace a provider result/error with a capture or exporter failure.
|
|
50
|
+
function safely(fn: () => void): void {
|
|
51
|
+
try {
|
|
52
|
+
fn();
|
|
53
|
+
} catch {
|
|
54
|
+
// Fail open, but never log the caught object: getters/exporters can quote user content.
|
|
55
|
+
try {
|
|
56
|
+
diag.warn(
|
|
57
|
+
"@traceai/google-genai-sdk: telemetry capture failed; SDK behavior is unchanged",
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
/* A user-supplied diagnostic logger must not break the SDK either. */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface MethodState {
|
|
66
|
+
original: Method;
|
|
67
|
+
descriptor?: PropertyDescriptor;
|
|
68
|
+
retired: boolean;
|
|
69
|
+
}
|
|
70
|
+
const methodStates = new WeakMap<Method, MethodState>();
|
|
71
|
+
|
|
72
|
+
function replaceMethod(
|
|
73
|
+
target: object,
|
|
74
|
+
name: string,
|
|
75
|
+
wrap: (original: Method) => Method,
|
|
76
|
+
): (() => void) | undefined {
|
|
77
|
+
const original = Reflect.get(target, name);
|
|
78
|
+
if (typeof original !== "function") return;
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, name);
|
|
80
|
+
// Frozen or non-configurable resources remain usable and unmodified.
|
|
81
|
+
if (
|
|
82
|
+
descriptor &&
|
|
83
|
+
!descriptor.configurable &&
|
|
84
|
+
(!descriptor.writable || !("value" in descriptor))
|
|
85
|
+
)
|
|
86
|
+
return;
|
|
87
|
+
if (!descriptor && !Object.isExtensible(target)) return;
|
|
88
|
+
const wrapped = wrap(original as Method);
|
|
89
|
+
const state: MethodState = {
|
|
90
|
+
original: original as Method,
|
|
91
|
+
descriptor,
|
|
92
|
+
retired: false,
|
|
93
|
+
};
|
|
94
|
+
methodStates.set(wrapped, state);
|
|
95
|
+
Object.defineProperty(
|
|
96
|
+
target,
|
|
97
|
+
name,
|
|
98
|
+
descriptor
|
|
99
|
+
? { ...descriptor, value: wrapped }
|
|
100
|
+
: { value: wrapped, writable: true, configurable: true },
|
|
101
|
+
);
|
|
102
|
+
return () => {
|
|
103
|
+
state.retired = true;
|
|
104
|
+
if (Reflect.get(target, name) !== wrapped) return;
|
|
105
|
+
// A disposed handle may finish underneath a newly installed handle. Skip
|
|
106
|
+
// its retired wrapper when restoring, rather than retaining a dead chain.
|
|
107
|
+
let prior = state;
|
|
108
|
+
while (methodStates.get(prior.original)?.retired)
|
|
109
|
+
prior = methodStates.get(prior.original)!;
|
|
110
|
+
if (prior.descriptor) Object.defineProperty(target, name, prior.descriptor);
|
|
111
|
+
else Reflect.deleteProperty(target, name);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const generationKey = Symbol("google.genai.generation");
|
|
116
|
+
type StartHop = (params: unknown) => (value: unknown) => void;
|
|
117
|
+
interface GenerationScope {
|
|
118
|
+
owner: symbol;
|
|
119
|
+
models: object;
|
|
120
|
+
start: StartHop;
|
|
121
|
+
}
|
|
122
|
+
const generationHooks = new WeakMap<
|
|
123
|
+
object,
|
|
124
|
+
{ owners: number; restore: (() => void)[] }
|
|
125
|
+
>();
|
|
126
|
+
|
|
127
|
+
/** Internal, version-tested boundaries: no public hook exposes every AFC/continuation hop. */
|
|
128
|
+
function retainGenerationHooks(models: object): () => void {
|
|
129
|
+
let hooks = generationHooks.get(models);
|
|
130
|
+
if (!hooks) {
|
|
131
|
+
hooks = { owners: 0, restore: [] };
|
|
132
|
+
try {
|
|
133
|
+
for (const name of [
|
|
134
|
+
"generateContentInternal",
|
|
135
|
+
"generateContentStreamInternal",
|
|
136
|
+
]) {
|
|
137
|
+
const restore = replaceMethod(
|
|
138
|
+
models,
|
|
139
|
+
name,
|
|
140
|
+
(original) =>
|
|
141
|
+
function (...args) {
|
|
142
|
+
const child = context.active();
|
|
143
|
+
const scope = child.getValue(generationKey) as
|
|
144
|
+
| GenerationScope
|
|
145
|
+
| undefined;
|
|
146
|
+
if (scope?.models !== models || isTracingSuppressed(child))
|
|
147
|
+
return Reflect.apply(original, this, args);
|
|
148
|
+
let observe = (_value: unknown): void => {};
|
|
149
|
+
safely(() => {
|
|
150
|
+
observe = scope.start(args[0]);
|
|
151
|
+
});
|
|
152
|
+
const result = Reflect.apply(original, this, args);
|
|
153
|
+
return Promise.resolve(result).then((value) => {
|
|
154
|
+
if (name === "generateContentStreamInternal") {
|
|
155
|
+
// Pull-driven: no prefetch, tool execution, extra requests or synthetic spans.
|
|
156
|
+
try {
|
|
157
|
+
return wrapStream(
|
|
158
|
+
value as object,
|
|
159
|
+
child,
|
|
160
|
+
observe,
|
|
161
|
+
() => {},
|
|
162
|
+
() => {},
|
|
163
|
+
);
|
|
164
|
+
} catch {
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
observe(value);
|
|
169
|
+
return value;
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
if (restore) hooks.restore.push(restore);
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
for (const restore of hooks.restore.reverse()) safely(restore);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
generationHooks.set(models, hooks);
|
|
180
|
+
}
|
|
181
|
+
hooks.owners++;
|
|
182
|
+
return () => {
|
|
183
|
+
if (--hooks.owners === 0) {
|
|
184
|
+
for (const restore of hooks.restore.reverse()) safely(restore);
|
|
185
|
+
generationHooks.delete(models);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const embeddingKey = Symbol("google.genai.embedding");
|
|
191
|
+
interface EmbeddingScope {
|
|
192
|
+
owner: symbol;
|
|
193
|
+
apiClient: object;
|
|
194
|
+
active: () => boolean;
|
|
195
|
+
observe: (value: unknown) => void;
|
|
196
|
+
cleanup: (restore: () => void) => void;
|
|
197
|
+
}
|
|
198
|
+
const embeddingHooks = new WeakMap<
|
|
199
|
+
object,
|
|
200
|
+
{ owners: number; restore?: () => void }
|
|
201
|
+
>();
|
|
202
|
+
|
|
203
|
+
/** Observe only the SDK's existing embedding parse, before its response converter
|
|
204
|
+
* drops usageMetadata. No global HttpResponse/fetch patch, second body read, or
|
|
205
|
+
* retained response payload. These per-client boundaries are version-tested.
|
|
206
|
+
*/
|
|
207
|
+
function retainEmbeddingHook(apiClient: object): () => void {
|
|
208
|
+
let hooks = embeddingHooks.get(apiClient);
|
|
209
|
+
if (!hooks) {
|
|
210
|
+
hooks = { owners: 0 };
|
|
211
|
+
hooks.restore = replaceMethod(apiClient, "request", original => function (...args) {
|
|
212
|
+
const child = context.active();
|
|
213
|
+
const scope = child.getValue(embeddingKey) as EmbeddingScope | undefined;
|
|
214
|
+
if (scope?.apiClient !== apiClient || !scope.active() || isTracingSuppressed(child))
|
|
215
|
+
return Reflect.apply(original, this, args);
|
|
216
|
+
// In the tested SDKs Gemini uses batchEmbedContents (or embedContent),
|
|
217
|
+
// while Vertex embedding calls use predict. Ignore all other requests.
|
|
218
|
+
let embeddingRequest = false;
|
|
219
|
+
safely(() => {
|
|
220
|
+
const path = record(args[0]).path;
|
|
221
|
+
embeddingRequest = typeof path === "string" &&
|
|
222
|
+
/:(?:batchEmbedContents|embedContent|predict)$/.test(path);
|
|
223
|
+
});
|
|
224
|
+
if (!embeddingRequest) return Reflect.apply(original, this, args);
|
|
225
|
+
const result = Reflect.apply(original, this, args);
|
|
226
|
+
return Promise.resolve(result).then(response => {
|
|
227
|
+
safely(() => {
|
|
228
|
+
if (!scope.active() || !response || typeof response !== "object") return;
|
|
229
|
+
let restore: (() => void) | undefined;
|
|
230
|
+
restore = replaceMethod(response, "json", parse => function (...parseArgs) {
|
|
231
|
+
// Restore before the one SDK-owned parse, even when it throws.
|
|
232
|
+
safely(() => restore?.());
|
|
233
|
+
const parsed = Reflect.apply(parse, this, parseArgs);
|
|
234
|
+
return Promise.resolve(parsed).then(value => {
|
|
235
|
+
if (scope.active()) safely(() => scope.observe(value));
|
|
236
|
+
return value;
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
if (restore) scope.cleanup(restore);
|
|
240
|
+
});
|
|
241
|
+
return response;
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
embeddingHooks.set(apiClient, hooks);
|
|
245
|
+
}
|
|
246
|
+
hooks.owners++;
|
|
247
|
+
return () => {
|
|
248
|
+
if (--hooks.owners === 0) {
|
|
249
|
+
safely(() => hooks.restore?.());
|
|
250
|
+
embeddingHooks.delete(apiClient);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Instruments one modern @google/genai client in place, including existing chats.
|
|
256
|
+
* Duplicate setup returns the existing handle (its original options win).
|
|
257
|
+
* No global module patch, private chat-field rewrite, exporter, or hidden API call is installed.
|
|
258
|
+
*/
|
|
259
|
+
export function instrumentGoogleGenAI(
|
|
260
|
+
client: { readonly models: object },
|
|
261
|
+
options: GoogleGenAITracingOptions = {},
|
|
262
|
+
): GoogleGenAITracing {
|
|
263
|
+
const existing = handles.get(client);
|
|
264
|
+
if (existing) return existing;
|
|
265
|
+
const config = generateTraceConfig(options.traceConfig);
|
|
266
|
+
const tracer = new FITracer({
|
|
267
|
+
tracer: (options.tracerProvider ?? trace.getTracerProvider()).getTracer(
|
|
268
|
+
"@traceai/google-genai-sdk",
|
|
269
|
+
VERSION,
|
|
270
|
+
),
|
|
271
|
+
traceConfig: config,
|
|
272
|
+
});
|
|
273
|
+
const requested = options.maxContentLength ?? 16384;
|
|
274
|
+
const limit = Number.isFinite(requested)
|
|
275
|
+
? Math.max(0, Math.min(65536, Math.floor(requested)))
|
|
276
|
+
: 16384;
|
|
277
|
+
let enabled = true;
|
|
278
|
+
let disposed = false;
|
|
279
|
+
let activeCalls = 0;
|
|
280
|
+
let releaseGeneration: (() => void) | undefined;
|
|
281
|
+
let releaseEmbedding: (() => void) | undefined;
|
|
282
|
+
let embeddingApiClient: object | undefined;
|
|
283
|
+
const restorers: (() => void)[] = [];
|
|
284
|
+
const accessorRestorers: (() => void)[] = [];
|
|
285
|
+
const owner = Symbol("google.genai.handle");
|
|
286
|
+
const restoreMethods = () => {
|
|
287
|
+
for (const restore of restorers.reverse()) safely(restore);
|
|
288
|
+
safely(() => releaseGeneration?.());
|
|
289
|
+
safely(() => releaseEmbedding?.());
|
|
290
|
+
};
|
|
291
|
+
const patched = new WeakSet<object>();
|
|
292
|
+
|
|
293
|
+
function replace(
|
|
294
|
+
target: object,
|
|
295
|
+
name: string,
|
|
296
|
+
wrap: (original: Method) => Method,
|
|
297
|
+
): void {
|
|
298
|
+
const restore = replaceMethod(target, name, wrap);
|
|
299
|
+
if (restore) restorers.push(restore);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function patch(target: object, methods: string[], resource: string): void {
|
|
303
|
+
if (patched.has(target)) return;
|
|
304
|
+
patched.add(target);
|
|
305
|
+
for (const name of methods) {
|
|
306
|
+
replace(
|
|
307
|
+
target,
|
|
308
|
+
name,
|
|
309
|
+
(original) =>
|
|
310
|
+
function (...args) {
|
|
311
|
+
const parent = context.active();
|
|
312
|
+
if (!enabled || disposed || isTracingSuppressed(parent)) {
|
|
313
|
+
const scope = parent.getValue(generationKey) as
|
|
314
|
+
| GenerationScope
|
|
315
|
+
| undefined;
|
|
316
|
+
let unobserved =
|
|
317
|
+
scope?.owner === owner
|
|
318
|
+
? parent.deleteValue(generationKey)
|
|
319
|
+
: parent;
|
|
320
|
+
if ((parent.getValue(embeddingKey) as EmbeddingScope | undefined)?.owner === owner)
|
|
321
|
+
unobserved = unobserved.deleteValue(embeddingKey);
|
|
322
|
+
return context.with(unobserved, () =>
|
|
323
|
+
Reflect.apply(original, this, args),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
activeCalls++;
|
|
327
|
+
let span: Span | undefined;
|
|
328
|
+
let capture: Capture | undefined;
|
|
329
|
+
let child = parent;
|
|
330
|
+
safely(() => {
|
|
331
|
+
span = tracer.startSpan(
|
|
332
|
+
`google.genai.${resource}.${name}`,
|
|
333
|
+
{
|
|
334
|
+
kind: SpanKind.CLIENT,
|
|
335
|
+
attributes: {
|
|
336
|
+
...getAttributesFromContext(parent),
|
|
337
|
+
[S.FI_SPAN_KIND]:
|
|
338
|
+
name === "embedContent"
|
|
339
|
+
? "EMBEDDING"
|
|
340
|
+
: resource === "interactions" && name !== "create"
|
|
341
|
+
? "CHAIN"
|
|
342
|
+
: "LLM",
|
|
343
|
+
[S.LLM_PROVIDER]: "google",
|
|
344
|
+
[S.GEN_AI_OPERATION_NAME]:
|
|
345
|
+
name === "generateVideos" ? "submit_video" : name,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
parent,
|
|
349
|
+
);
|
|
350
|
+
child = trace.setSpan(parent, span);
|
|
351
|
+
capture = new Capture(span, config, limit);
|
|
352
|
+
const params = record(args[0]);
|
|
353
|
+
capture.input(params, name);
|
|
354
|
+
if (resource === "interactions" && name !== "create")
|
|
355
|
+
capture.string("google.genai.interaction_id", args[0]);
|
|
356
|
+
});
|
|
357
|
+
let ended = false;
|
|
358
|
+
if (
|
|
359
|
+
resource === "models" &&
|
|
360
|
+
(name === "generateContent" || name === "generateContentStream")
|
|
361
|
+
) {
|
|
362
|
+
const start: StartHop = (params) => {
|
|
363
|
+
const observe = !ended
|
|
364
|
+
? capture?.generationHop(record(params))
|
|
365
|
+
: undefined;
|
|
366
|
+
return (value) => {
|
|
367
|
+
if (!ended) safely(() => observe?.(value));
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
child = child.setValue(generationKey, {
|
|
371
|
+
owner,
|
|
372
|
+
models: client.models,
|
|
373
|
+
start,
|
|
374
|
+
} satisfies GenerationScope);
|
|
375
|
+
}
|
|
376
|
+
const responseCleanups: (() => void)[] = [];
|
|
377
|
+
child = child.deleteValue(embeddingKey);
|
|
378
|
+
if (resource === "models" && name === "embedContent" && embeddingApiClient) {
|
|
379
|
+
child = child.setValue(embeddingKey, {
|
|
380
|
+
owner,
|
|
381
|
+
apiClient: embeddingApiClient,
|
|
382
|
+
active: () => !ended,
|
|
383
|
+
observe: value => capture?.embeddingUsage(value),
|
|
384
|
+
cleanup: restore => responseCleanups.push(restore),
|
|
385
|
+
} satisfies EmbeddingScope);
|
|
386
|
+
}
|
|
387
|
+
const abortCleanups: (() => void)[] = [];
|
|
388
|
+
const finish = (error = false, cancelled = false) => {
|
|
389
|
+
if (ended) return;
|
|
390
|
+
ended = true;
|
|
391
|
+
for (const cleanup of abortCleanups) safely(cleanup);
|
|
392
|
+
for (const cleanup of responseCleanups) safely(cleanup);
|
|
393
|
+
responseCleanups.length = 0;
|
|
394
|
+
safely(() => capture?.finish());
|
|
395
|
+
safely(() => {
|
|
396
|
+
if (cancelled)
|
|
397
|
+
span?.setAttribute("google.genai.cancelled", true);
|
|
398
|
+
});
|
|
399
|
+
// Provider error bodies and stack traces can contain keys or media even when content capture is enabled.
|
|
400
|
+
safely(() => {
|
|
401
|
+
if (error) {
|
|
402
|
+
span?.setAttribute("error.type", "provider_error");
|
|
403
|
+
span?.setStatus({ code: SpanStatusCode.ERROR });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
safely(() => span?.end());
|
|
407
|
+
if (--activeCalls === 0 && disposed) restoreMethods();
|
|
408
|
+
};
|
|
409
|
+
const watchAbort = (signal: unknown, deferred = false) =>
|
|
410
|
+
safely(() => {
|
|
411
|
+
if (ended || !(signal instanceof AbortSignal)) return;
|
|
412
|
+
const onAbort = () => {
|
|
413
|
+
if (!deferred) return finish(false, true);
|
|
414
|
+
// Legacy stream cleanup also aborts on errors. Let a pending rejection
|
|
415
|
+
// retain its ERROR status before treating an idle controller abort as cancellation.
|
|
416
|
+
const pending = setImmediate(() => finish(false, true));
|
|
417
|
+
abortCleanups.push(() => clearImmediate(pending));
|
|
418
|
+
};
|
|
419
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
420
|
+
abortCleanups.push(() =>
|
|
421
|
+
signal.removeEventListener("abort", onAbort),
|
|
422
|
+
);
|
|
423
|
+
if (signal.aborted) onAbort();
|
|
424
|
+
});
|
|
425
|
+
safely(() =>
|
|
426
|
+
watchAbort(
|
|
427
|
+
record(record(args[0]).config).abortSignal ??
|
|
428
|
+
record(args.at(-1)).signal,
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
const observe = (value: unknown) => {
|
|
432
|
+
if (ended) return;
|
|
433
|
+
safely(() => {
|
|
434
|
+
capture?.response(value);
|
|
435
|
+
const status =
|
|
436
|
+
record(record(value).interaction).status ??
|
|
437
|
+
record(value).status;
|
|
438
|
+
if (
|
|
439
|
+
status === "failed" ||
|
|
440
|
+
record(value).event_type === "error"
|
|
441
|
+
) {
|
|
442
|
+
span?.setAttribute("google.genai.status", "failed");
|
|
443
|
+
span?.setAttribute("error.type", "provider_error");
|
|
444
|
+
span?.setStatus({ code: SpanStatusCode.ERROR });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
const resolvedStreams = new WeakMap<object, object>();
|
|
449
|
+
const resolve = (value: unknown): unknown => {
|
|
450
|
+
try {
|
|
451
|
+
if (
|
|
452
|
+
value &&
|
|
453
|
+
typeof Reflect.get(Object(value), Symbol.asyncIterator) ===
|
|
454
|
+
"function"
|
|
455
|
+
) {
|
|
456
|
+
const existingStream = resolvedStreams.get(value as object);
|
|
457
|
+
if (existingStream) return existingStream;
|
|
458
|
+
watchAbort(record(record(value).controller).signal, true);
|
|
459
|
+
const wrappedStream = wrapStream(
|
|
460
|
+
value as object,
|
|
461
|
+
child,
|
|
462
|
+
observe,
|
|
463
|
+
finish,
|
|
464
|
+
() => {
|
|
465
|
+
safely(() =>
|
|
466
|
+
span?.setAttribute(
|
|
467
|
+
"google.genai.stream_capture",
|
|
468
|
+
"request_only",
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
finish();
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
resolvedStreams.set(value as object, wrappedStream);
|
|
475
|
+
return wrappedStream;
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
/* Fall back to the original value if instrumentation cannot wrap it. */
|
|
479
|
+
}
|
|
480
|
+
observe(value);
|
|
481
|
+
finish();
|
|
482
|
+
return value;
|
|
483
|
+
};
|
|
484
|
+
try {
|
|
485
|
+
const result = context.with(child, () =>
|
|
486
|
+
Reflect.apply(original, this, args),
|
|
487
|
+
);
|
|
488
|
+
if (result && typeof record(result).then === "function") {
|
|
489
|
+
const reject = (error: unknown): never => {
|
|
490
|
+
finish(true);
|
|
491
|
+
throw error;
|
|
492
|
+
};
|
|
493
|
+
if (
|
|
494
|
+
typeof record(result).withResponse === "function" ||
|
|
495
|
+
typeof record(result).asResponse === "function"
|
|
496
|
+
) {
|
|
497
|
+
return wrapAPIPromise(
|
|
498
|
+
result as object,
|
|
499
|
+
child,
|
|
500
|
+
resolve,
|
|
501
|
+
reject,
|
|
502
|
+
() => {
|
|
503
|
+
safely(() =>
|
|
504
|
+
span?.setAttribute(
|
|
505
|
+
"google.genai.response_capture",
|
|
506
|
+
"request_only",
|
|
507
|
+
),
|
|
508
|
+
);
|
|
509
|
+
finish();
|
|
510
|
+
},
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return Promise.resolve(result).then(resolve, reject);
|
|
514
|
+
}
|
|
515
|
+
return resolve(result);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
finish(true);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
releaseGeneration = retainGenerationHooks(client.models);
|
|
526
|
+
safely(() => {
|
|
527
|
+
const apiClient = record(client.models).apiClient;
|
|
528
|
+
if (apiClient && typeof apiClient === "object") {
|
|
529
|
+
releaseEmbedding = retainEmbeddingHook(apiClient);
|
|
530
|
+
embeddingApiClient = apiClient;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
patch(client.models, modelMethods, "models");
|
|
534
|
+
// Preserve the public accessor and resource identity, without private SDK field writes.
|
|
535
|
+
let owner: object | null = client;
|
|
536
|
+
let interactionDescriptor: PropertyDescriptor | undefined;
|
|
537
|
+
while (owner && !interactionDescriptor) {
|
|
538
|
+
interactionDescriptor = Object.getOwnPropertyDescriptor(
|
|
539
|
+
owner,
|
|
540
|
+
"interactions",
|
|
541
|
+
);
|
|
542
|
+
if (!interactionDescriptor) owner = Object.getPrototypeOf(owner);
|
|
543
|
+
}
|
|
544
|
+
if (interactionDescriptor?.get && Object.isExtensible(client)) {
|
|
545
|
+
const own = Object.getOwnPropertyDescriptor(client, "interactions");
|
|
546
|
+
const getter = interactionDescriptor.get;
|
|
547
|
+
const wrappedGetter = function (this: object): unknown {
|
|
548
|
+
const resource = getter.call(this);
|
|
549
|
+
if (
|
|
550
|
+
!disposed &&
|
|
551
|
+
this === client &&
|
|
552
|
+
resource &&
|
|
553
|
+
typeof resource === "object"
|
|
554
|
+
)
|
|
555
|
+
safely(() => patch(resource, interactionMethods, "interactions"));
|
|
556
|
+
return resource;
|
|
557
|
+
};
|
|
558
|
+
Object.defineProperty(client, "interactions", {
|
|
559
|
+
configurable: true,
|
|
560
|
+
enumerable: interactionDescriptor.enumerable,
|
|
561
|
+
get: wrappedGetter,
|
|
562
|
+
});
|
|
563
|
+
// Public accessor only: also cover a resource reference obtained before setup.
|
|
564
|
+
// Unsupported backend/version getters may throw; generation must still work.
|
|
565
|
+
safely(() => wrappedGetter.call(client));
|
|
566
|
+
accessorRestorers.push(() => {
|
|
567
|
+
if (
|
|
568
|
+
Object.getOwnPropertyDescriptor(client, "interactions")?.get !==
|
|
569
|
+
wrappedGetter
|
|
570
|
+
)
|
|
571
|
+
return;
|
|
572
|
+
if (own) Object.defineProperty(client, "interactions", own);
|
|
573
|
+
else Reflect.deleteProperty(client, "interactions");
|
|
574
|
+
});
|
|
575
|
+
} else if (
|
|
576
|
+
interactionDescriptor &&
|
|
577
|
+
"value" in interactionDescriptor &&
|
|
578
|
+
interactionDescriptor.value
|
|
579
|
+
) {
|
|
580
|
+
patch(interactionDescriptor.value, interactionMethods, "interactions");
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
for (const restore of accessorRestorers.reverse()) safely(restore);
|
|
584
|
+
restoreMethods();
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
const handle: GoogleGenAITracing = {
|
|
588
|
+
disable() {
|
|
589
|
+
enabled = false;
|
|
590
|
+
},
|
|
591
|
+
enable() {
|
|
592
|
+
if (!disposed) enabled = true;
|
|
593
|
+
},
|
|
594
|
+
dispose() {
|
|
595
|
+
if (disposed) return;
|
|
596
|
+
disposed = true;
|
|
597
|
+
for (const restore of accessorRestorers.reverse()) safely(restore);
|
|
598
|
+
handles.delete(client);
|
|
599
|
+
if (activeCalls === 0) restoreMethods();
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
handles.set(client, handle);
|
|
603
|
+
return handle;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Pull-driven: do not read ahead, and keep next/return/throw under the call's context. */
|
|
607
|
+
function wrapStream(
|
|
608
|
+
target: object,
|
|
609
|
+
child: Context,
|
|
610
|
+
observe: (value: unknown) => void,
|
|
611
|
+
finish: (error?: boolean, cancelled?: boolean) => void,
|
|
612
|
+
requestOnly: () => void,
|
|
613
|
+
): object {
|
|
614
|
+
const iterators = new WeakMap<object, object>();
|
|
615
|
+
function proxy(source: object): object {
|
|
616
|
+
const existing = iterators.get(source);
|
|
617
|
+
if (existing) return existing;
|
|
618
|
+
const wrapped = new Proxy(source, {
|
|
619
|
+
get(object, key) {
|
|
620
|
+
const method = Reflect.get(object, key, object);
|
|
621
|
+
if (typeof method !== "function") return method;
|
|
622
|
+
if (key === Symbol.asyncIterator)
|
|
623
|
+
return () => proxy(Reflect.apply(method, object, []));
|
|
624
|
+
if (key === "getReader" || key === "values")
|
|
625
|
+
return (...args: unknown[]) =>
|
|
626
|
+
proxy(Reflect.apply(method, object, args));
|
|
627
|
+
// The legacy helper consumes this[Symbol.asyncIterator], so keep it on our observed path.
|
|
628
|
+
if (key === "toReadableStream") return method.bind(wrapped);
|
|
629
|
+
// Native piping/branching bypasses public reads. Preserve it unchanged and
|
|
630
|
+
// end an explicitly request-only span rather than leave an orphan open forever.
|
|
631
|
+
if (["pipeTo", "pipeThrough", "tee"].includes(String(key)))
|
|
632
|
+
return (...args: unknown[]) => {
|
|
633
|
+
const result = Reflect.apply(method, object, args);
|
|
634
|
+
requestOnly();
|
|
635
|
+
return result;
|
|
636
|
+
};
|
|
637
|
+
if (
|
|
638
|
+
!["next", "read", "return", "throw", "cancel"].includes(String(key))
|
|
639
|
+
)
|
|
640
|
+
return method.bind(object);
|
|
641
|
+
return (...args: unknown[]) => {
|
|
642
|
+
try {
|
|
643
|
+
return Promise.resolve(
|
|
644
|
+
context.with(child, () => Reflect.apply(method, object, args)),
|
|
645
|
+
).then(
|
|
646
|
+
(result) => {
|
|
647
|
+
if (key === "cancel" || record(result).done)
|
|
648
|
+
finish(false, key === "return" || key === "cancel");
|
|
649
|
+
else observe(record(result).value);
|
|
650
|
+
return result;
|
|
651
|
+
},
|
|
652
|
+
(error) => {
|
|
653
|
+
finish(true);
|
|
654
|
+
throw error;
|
|
655
|
+
},
|
|
656
|
+
);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
finish(true);
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
iterators.set(source, wrapped);
|
|
665
|
+
return wrapped;
|
|
666
|
+
}
|
|
667
|
+
return proxy(target);
|
|
668
|
+
}
|