@telemetry-dev/bedrock 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.
@@ -0,0 +1,301 @@
1
+ import { startSpan, type SpanFields, type SpanHandle, type SpanType } from "@telemetry-dev/sdk";
2
+
3
+ export const PROVIDER = "amazon-bedrock";
4
+
5
+ const STREAM_FINALIZER =
6
+ typeof FinalizationRegistry === "function"
7
+ ? new FinalizationRegistry<() => void>((finish) => finish())
8
+ : undefined;
9
+
10
+ export interface BedrockInstrumentationOptions {
11
+ captureAgentTrace?: boolean;
12
+ }
13
+
14
+ export type SendFn = (...args: unknown[]) => unknown;
15
+
16
+ export interface OpHandler {
17
+ spanName(input: Record<string, unknown>): string;
18
+ spanType(input: Record<string, unknown>): SpanType;
19
+ requestFields(input: Record<string, unknown>, options: BedrockInstrumentationOptions): SpanFields;
20
+ onResult(
21
+ result: unknown,
22
+ span: SpanHandle,
23
+ t0: number,
24
+ input: Record<string, unknown>,
25
+ options: BedrockInstrumentationOptions,
26
+ ): unknown;
27
+ }
28
+
29
+ export function createInstrumentedSend(
30
+ originalSend: SendFn,
31
+ options: BedrockInstrumentationOptions,
32
+ handlers: Record<string, OpHandler>,
33
+ ): SendFn {
34
+ return function instrumentedSend(this: unknown, command: unknown, ...rest: unknown[]) {
35
+ const name = command?.constructor?.name;
36
+ const handler = typeof name === "string" ? handlers[name] : undefined;
37
+ if (!handler) {
38
+ return originalSend.apply(this, [command, ...rest]);
39
+ }
40
+
41
+ const input = commandInput(command);
42
+ const t0 = performance.now();
43
+ const spanType = safe(() => handler.spanType(input)) ?? "span";
44
+ const spanName = safe(() => handler.spanName(input)) ?? name ?? "bedrock";
45
+ const fields = safe(() => handler.requestFields(input, options)) ?? {};
46
+ const span = startSpan(spanName, { type: spanType, ...fields });
47
+ const callbackIndex = rest.findIndex((arg) => typeof arg === "function");
48
+ if (callbackIndex !== -1) {
49
+ const callback = rest[callbackIndex] as (error: unknown, data?: unknown) => void;
50
+ const wrappedRest = [...rest];
51
+ wrappedRest[callbackIndex] = (error: unknown, data?: unknown) => {
52
+ if (error != null) {
53
+ endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
54
+ callback(error, data);
55
+ return;
56
+ }
57
+ let nextData = data;
58
+ try {
59
+ nextData = handler.onResult(data, span, t0, input, options);
60
+ } catch {
61
+ endSpan(span, awsMetadataFields(metadataOf(data)));
62
+ }
63
+ callback(error, nextData);
64
+ };
65
+ try {
66
+ return originalSend.apply(this, [command, ...wrappedRest]);
67
+ } catch (error) {
68
+ endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ let result: unknown;
74
+ try {
75
+ result = originalSend.apply(this, [command, ...rest]);
76
+ } catch (error) {
77
+ endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
78
+ throw error;
79
+ }
80
+
81
+ return Promise.resolve(result).then(
82
+ (resolved) => {
83
+ try {
84
+ return handler.onResult(resolved, span, t0, input, options);
85
+ } catch {
86
+ endSpan(span, awsMetadataFields(metadataOf(resolved)));
87
+ return resolved;
88
+ }
89
+ },
90
+ (error) => {
91
+ endSpan(span, { ...awsMetadataFields(metadataOf(error)), error });
92
+ throw error;
93
+ },
94
+ );
95
+ };
96
+ }
97
+
98
+ export function commandInput(command: unknown): Record<string, unknown> {
99
+ const value = (command as { input?: unknown } | null | undefined)?.input;
100
+ return isRecord(value) ? value : {};
101
+ }
102
+
103
+ export function isRecord(value: unknown): value is Record<string, unknown> {
104
+ return value !== null && typeof value === "object" && !Array.isArray(value);
105
+ }
106
+
107
+ export function safe<T>(fn: () => T): T | undefined {
108
+ try {
109
+ return fn();
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ export function endSpan(span: SpanHandle, fields?: SpanFields): void {
116
+ try {
117
+ span.end(fields);
118
+ } catch {
119
+ // Instrumentation must never throw into caller code.
120
+ }
121
+ }
122
+
123
+ export function updateSpan(span: SpanHandle, fields: SpanFields): void {
124
+ try {
125
+ span.update(fields);
126
+ } catch {
127
+ // Instrumentation must never throw into caller code.
128
+ }
129
+ }
130
+
131
+ export function metadataOf(value: unknown): unknown {
132
+ if (isRecord(value) && isRecord(value.$metadata)) return value.$metadata;
133
+ return (value as { $metadata?: unknown } | null | undefined)?.$metadata;
134
+ }
135
+
136
+ export function modeledStreamError(
137
+ event: Record<string, unknown>,
138
+ ): { error: Error; fields: SpanFields } | undefined {
139
+ const keys: Record<string, string> = {
140
+ accessDeniedException: "AccessDeniedException",
141
+ badGatewayException: "BadGatewayException",
142
+ conflictException: "ConflictException",
143
+ dependencyFailedException: "DependencyFailedException",
144
+ internalServerException: "InternalServerException",
145
+ modelNotReadyException: "ModelNotReadyException",
146
+ modelStreamErrorException: "ModelStreamErrorException",
147
+ modelTimeoutException: "ModelTimeoutException",
148
+ resourceNotFoundException: "ResourceNotFoundException",
149
+ serviceQuotaExceededException: "ServiceQuotaExceededException",
150
+ serviceUnavailableException: "ServiceUnavailableException",
151
+ throttlingException: "ThrottlingException",
152
+ validationException: "ValidationException",
153
+ };
154
+ for (const [key, name] of Object.entries(keys)) {
155
+ const exception = isRecord(event[key]) ? event[key] : undefined;
156
+ if (!exception) continue;
157
+ const error = new Error(
158
+ stringValue(exception.message) ?? stringValue(exception.originalMessage) ?? name,
159
+ );
160
+ error.name = stringValue(exception.name) ?? name;
161
+ return { error, fields: awsMetadataFields(exception.$metadata) };
162
+ }
163
+ return undefined;
164
+ }
165
+
166
+ export function awsMetadataFields(metadata: unknown): SpanFields {
167
+ if (!isRecord(metadata)) return {};
168
+ const requestId = stringValue(metadata.requestId);
169
+ const attempts = numberValue(metadata.attempts);
170
+ const httpStatusCode = numberValue(metadata.httpStatusCode);
171
+ const totalRetryDelay = numberValue(metadata.totalRetryDelay);
172
+ const attributes: SpanFields["attributes"] = {};
173
+ if (httpStatusCode !== undefined) attributes["aws.http.status_code"] = httpStatusCode;
174
+ if (attempts !== undefined && attempts > 1) attributes["aws.request.attempts"] = attempts;
175
+ if (totalRetryDelay !== undefined) {
176
+ attributes["aws.request.total_retry_delay_ms"] = totalRetryDelay;
177
+ }
178
+ return omitUndefined({
179
+ responseId: requestId,
180
+ attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
181
+ });
182
+ }
183
+
184
+ export function mergeFields(...fields: Array<SpanFields | undefined>): SpanFields {
185
+ const merged: SpanFields = {};
186
+ for (const field of fields) {
187
+ if (!field) continue;
188
+ const { metadata, attributes, usage, ...rest } = field;
189
+ Object.assign(merged, omitUndefined(rest));
190
+ if (usage) merged.usage = { ...merged.usage, ...usage };
191
+ if (metadata) merged.metadata = { ...merged.metadata, ...metadata };
192
+ if (attributes) merged.attributes = { ...merged.attributes, ...attributes };
193
+ }
194
+ return merged;
195
+ }
196
+
197
+ export function omitUndefined<T extends Record<string, unknown>>(value: T): T {
198
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T;
199
+ }
200
+
201
+ export function stringValue(value: unknown): string | undefined {
202
+ return typeof value === "string" && value.length > 0 ? value : undefined;
203
+ }
204
+
205
+ export function numberValue(value: unknown): number | undefined {
206
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
207
+ }
208
+
209
+ export function arrayValue<T>(value: unknown): T[] | undefined {
210
+ return Array.isArray(value) ? (value as T[]) : undefined;
211
+ }
212
+
213
+ export function bytesToString(value: unknown): string | undefined {
214
+ if (typeof value === "string") return value;
215
+ if (value instanceof Uint8Array) return new TextDecoder().decode(value);
216
+ if (ArrayBuffer.isView(value)) return new TextDecoder().decode(value as Uint8Array);
217
+ return undefined;
218
+ }
219
+
220
+ export function parseJson(value: unknown): unknown {
221
+ const text = bytesToString(value);
222
+ if (text === undefined) return undefined;
223
+ try {
224
+ return JSON.parse(text) as unknown;
225
+ } catch {
226
+ return undefined;
227
+ }
228
+ }
229
+
230
+ export interface StreamState {
231
+ feed(event: unknown): void;
232
+ finish(partial: boolean): SpanFields;
233
+ }
234
+
235
+ export function wrapAsyncIterable<T>(
236
+ iterable: T,
237
+ state: StreamState,
238
+ span: SpanHandle,
239
+ t0: number,
240
+ baseFields: SpanFields,
241
+ ): T {
242
+ if (
243
+ !iterable ||
244
+ typeof (iterable as unknown as AsyncIterable<unknown>)[Symbol.asyncIterator] !== "function"
245
+ ) {
246
+ endSpan(span, baseFields);
247
+ return iterable;
248
+ }
249
+
250
+ let ended = false;
251
+ let first = false;
252
+ const unregisterToken = {};
253
+ const endOnce = (fields?: SpanFields) => {
254
+ if (ended) return;
255
+ ended = true;
256
+ STREAM_FINALIZER?.unregister(unregisterToken);
257
+ endSpan(span, mergeFields(baseFields, fields));
258
+ };
259
+
260
+ const source = iterable as unknown as AsyncIterable<unknown>;
261
+ const generator = async function* () {
262
+ let completed = false;
263
+ try {
264
+ for await (const event of source) {
265
+ if (!first) {
266
+ first = true;
267
+ updateSpan(span, { timeToFirstChunkMs: performance.now() - t0 });
268
+ }
269
+ safe(() => state.feed(event));
270
+ yield event;
271
+ }
272
+ completed = true;
273
+ } catch (error) {
274
+ endOnce(mergeFields(state.finish(true), awsMetadataFields(metadataOf(error)), { error }));
275
+ throw error;
276
+ } finally {
277
+ if (!ended) endOnce(state.finish(!completed));
278
+ }
279
+ };
280
+
281
+ const wrapped: Record<PropertyKey, unknown> = {};
282
+ if (isRecord(iterable)) {
283
+ for (const key of Reflect.ownKeys(iterable)) {
284
+ wrapped[key] = (iterable as Record<PropertyKey, unknown>)[key];
285
+ }
286
+ }
287
+ const finishAbandoned = () => endOnce(safe(() => state.finish(true)));
288
+ wrapped[Symbol.asyncIterator] = () => {
289
+ const iterator = generator();
290
+ STREAM_FINALIZER?.register(iterator, finishAbandoned, unregisterToken);
291
+ return iterator;
292
+ };
293
+ STREAM_FINALIZER?.register(wrapped, finishAbandoned, unregisterToken);
294
+ return wrapped as T;
295
+ }
296
+
297
+ export function basenameArn(value: unknown): string | undefined {
298
+ const text = stringValue(value);
299
+ if (!text) return undefined;
300
+ return text.split(/[/:]/).filter(Boolean).at(-1) ?? text;
301
+ }
@@ -0,0 +1,332 @@
1
+ import type { SpanFields } from "@telemetry-dev/sdk";
2
+
3
+ import {
4
+ awsMetadataFields,
5
+ isRecord,
6
+ mergeFields,
7
+ modeledStreamError,
8
+ numberValue,
9
+ omitUndefined,
10
+ parseJson,
11
+ PROVIDER,
12
+ stringValue,
13
+ type StreamState,
14
+ } from "./internal.ts";
15
+
16
+ export function isEmbeddingModel(modelId: unknown): boolean {
17
+ const model = stringValue(modelId)?.toLowerCase() ?? "";
18
+ return (
19
+ model.includes("titan-embed") || model.includes("cohere.embed") || model.includes("-embedding")
20
+ );
21
+ }
22
+
23
+ export function invokeModelSpanName(input: Record<string, unknown>): string {
24
+ const model = stringValue(input.modelId) ?? "unknown";
25
+ return `${isEmbeddingModel(model) ? "embeddings" : "chat"} ${model}`;
26
+ }
27
+
28
+ export function invokeModelRequestFields(input: Record<string, unknown>): SpanFields {
29
+ const model = stringValue(input.modelId);
30
+ const body = parseBody(input.body, input.contentType);
31
+ return omitUndefined({
32
+ provider: PROVIDER,
33
+ model,
34
+ input: body,
35
+ outputType: isEmbeddingModel(model) ? "embedding" : undefined,
36
+ ...nativeSamplingFields(model, body),
37
+ });
38
+ }
39
+
40
+ export function invokeModelResponseFields(
41
+ input: Record<string, unknown>,
42
+ output: unknown,
43
+ ): SpanFields {
44
+ const model = stringValue(input.modelId);
45
+ const body = isRecord(output) ? parseBody(output.body, output.contentType) : undefined;
46
+ const metadata = isRecord(output) ? output.$metadata : undefined;
47
+ return mergeFields(awsMetadataFields(metadata), nativeResponseFields(model, body, metadata));
48
+ }
49
+
50
+ export function parseBody(body: unknown, contentType: unknown): unknown {
51
+ const type = stringValue(contentType) ?? "application/json";
52
+ if (!type.includes("json")) return undefined;
53
+ return parseJson(body);
54
+ }
55
+
56
+ function nativeSamplingFields(model: string | undefined, body: unknown): SpanFields {
57
+ if (!isRecord(body)) return {};
58
+ const lower = model?.toLowerCase() ?? "";
59
+ if (lower.includes("amazon.titan")) {
60
+ const cfg = isRecord(body.textGenerationConfig) ? body.textGenerationConfig : {};
61
+ return omitUndefined({
62
+ temperature: numberValue(cfg.temperature),
63
+ topP: numberValue(cfg.topP),
64
+ maxTokens: numberValue(cfg.maxTokenCount),
65
+ stopSequences: Array.isArray(cfg.stopSequences) ? (cfg.stopSequences as string[]) : undefined,
66
+ });
67
+ }
68
+ if (lower.includes("amazon.nova")) {
69
+ const cfg = isRecord(body.inferenceConfig) ? body.inferenceConfig : {};
70
+ return omitUndefined({
71
+ temperature: numberValue(cfg.temperature),
72
+ topP: numberValue(cfg.topP) ?? numberValue(cfg.top_p),
73
+ topK: numberValue(cfg.topK),
74
+ maxTokens: numberValue(cfg.maxTokens) ?? numberValue(cfg.max_new_tokens),
75
+ stopSequences: Array.isArray(cfg.stopSequences) ? (cfg.stopSequences as string[]) : undefined,
76
+ });
77
+ }
78
+ if (lower.includes("anthropic.claude")) {
79
+ return omitUndefined({
80
+ temperature: numberValue(body.temperature),
81
+ topP: numberValue(body.top_p),
82
+ topK: numberValue(body.top_k),
83
+ maxTokens: numberValue(body.max_tokens),
84
+ stopSequences: Array.isArray(body.stop_sequences)
85
+ ? (body.stop_sequences as string[])
86
+ : undefined,
87
+ });
88
+ }
89
+ if (lower.includes("meta.llama")) {
90
+ return omitUndefined({
91
+ temperature: numberValue(body.temperature),
92
+ topP: numberValue(body.top_p),
93
+ maxTokens: numberValue(body.max_gen_len),
94
+ });
95
+ }
96
+ return omitUndefined({
97
+ temperature: numberValue(body.temperature),
98
+ topP: numberValue(body.top_p) ?? numberValue(body.topP),
99
+ maxTokens: numberValue(body.max_tokens) ?? numberValue(body.maxTokens),
100
+ });
101
+ }
102
+
103
+ function nativeResponseFields(
104
+ model: string | undefined,
105
+ body: unknown,
106
+ metadata: unknown,
107
+ ): SpanFields {
108
+ if (!isRecord(body)) return headerUsageFields(metadata);
109
+ const lower = model?.toLowerCase() ?? "";
110
+ if (isEmbeddingModel(model)) {
111
+ return withHeaderUsage(
112
+ {
113
+ output: body,
114
+ outputType: "embedding",
115
+ usage: omitUndefined({ inputTokens: numberValue(body.inputTextTokenCount) }),
116
+ },
117
+ metadata,
118
+ );
119
+ }
120
+ if (lower.includes("amazon.titan")) {
121
+ const first = Array.isArray(body.results) && isRecord(body.results[0]) ? body.results[0] : {};
122
+ return omitUndefined({
123
+ output: body,
124
+ finishReason: stringValue(first.completionReason),
125
+ usage: omitUndefined({
126
+ inputTokens: numberValue(body.inputTextTokenCount),
127
+ outputTokens: numberValue(first.tokenCount),
128
+ }),
129
+ });
130
+ }
131
+ if (lower.includes("amazon.nova")) {
132
+ const usage = isRecord(body.usage) ? body.usage : {};
133
+ return omitUndefined({
134
+ output: body,
135
+ finishReason: stringValue(body.stopReason),
136
+ usage: omitUndefined({
137
+ inputTokens: numberValue(usage.inputTokens),
138
+ outputTokens: numberValue(usage.outputTokens),
139
+ totalTokens: numberValue(usage.totalTokens),
140
+ }),
141
+ });
142
+ }
143
+ if (lower.includes("anthropic.claude")) {
144
+ const usage = isRecord(body.usage) ? body.usage : {};
145
+ return withHeaderUsage(
146
+ {
147
+ output: body,
148
+ finishReason: stringValue(body.stop_reason),
149
+ usage: omitUndefined({
150
+ inputTokens: numberValue(usage.input_tokens),
151
+ outputTokens: numberValue(usage.output_tokens),
152
+ cacheReadInputTokens: numberValue(usage.cache_read_input_tokens),
153
+ cacheCreationInputTokens: numberValue(usage.cache_creation_input_tokens),
154
+ }),
155
+ },
156
+ metadata,
157
+ );
158
+ }
159
+ if (lower.includes("meta.llama")) {
160
+ return omitUndefined({
161
+ output: body,
162
+ finishReason: stringValue(body.stop_reason),
163
+ usage: omitUndefined({
164
+ inputTokens: numberValue(body.prompt_token_count),
165
+ outputTokens: numberValue(body.generation_token_count),
166
+ }),
167
+ });
168
+ }
169
+ if (lower.includes("mistral")) {
170
+ const first = Array.isArray(body.outputs) && isRecord(body.outputs[0]) ? body.outputs[0] : {};
171
+ return withHeaderUsage(
172
+ { output: body, finishReason: stringValue(first.stop_reason) },
173
+ metadata,
174
+ );
175
+ }
176
+ if (lower.includes("cohere.command-r")) {
177
+ return withHeaderUsage(
178
+ { output: body, finishReason: stringValue(body.finish_reason) },
179
+ metadata,
180
+ );
181
+ }
182
+ if (lower.includes("cohere.command")) {
183
+ const first =
184
+ Array.isArray(body.generations) && isRecord(body.generations[0]) ? body.generations[0] : {};
185
+ return withHeaderUsage(
186
+ { output: body, finishReason: stringValue(first.finish_reason) },
187
+ metadata,
188
+ );
189
+ }
190
+ return withHeaderUsage({ output: body }, metadata);
191
+ }
192
+
193
+ function withHeaderUsage(fields: SpanFields, metadata: unknown): SpanFields {
194
+ if (fields.usage && Object.keys(fields.usage).length > 0) return fields;
195
+ return mergeFields(fields, headerUsageFields(metadata));
196
+ }
197
+
198
+ function headerUsageFields(metadata: unknown): SpanFields {
199
+ const headers = isRecord(metadata)
200
+ ? isRecord(metadata.httpHeaders)
201
+ ? metadata.httpHeaders
202
+ : isRecord(metadata.HTTPHeaders)
203
+ ? metadata.HTTPHeaders
204
+ : undefined
205
+ : undefined;
206
+ const usage = omitUndefined({
207
+ inputTokens: headerNumber(headers?.["x-amzn-bedrock-input-token-count"]),
208
+ outputTokens: headerNumber(headers?.["x-amzn-bedrock-output-token-count"]),
209
+ });
210
+ return { usage: Object.keys(usage).length > 0 ? usage : undefined };
211
+ }
212
+
213
+ function headerNumber(value: unknown): number | undefined {
214
+ if (typeof value === "string") {
215
+ const parsed = Number.parseInt(value, 10);
216
+ return Number.isFinite(parsed) ? parsed : undefined;
217
+ }
218
+ return numberValue(value);
219
+ }
220
+
221
+ export class InvokeModelStreamState implements StreamState {
222
+ private readonly chunks: unknown[] = [];
223
+ private text = "";
224
+ private usage: SpanFields["usage"];
225
+ private finishReason: string | undefined;
226
+ private error: Error | undefined;
227
+ private errorFields: SpanFields | undefined;
228
+ feed(event: unknown): void {
229
+ if (!isRecord(event)) return;
230
+ const streamError = modeledStreamError(event);
231
+ if (streamError) {
232
+ this.error = streamError.error;
233
+ this.errorFields = streamError.fields;
234
+ return;
235
+ }
236
+ const chunk = isRecord(event.chunk) ? event.chunk : undefined;
237
+ const parsed = parseJson(chunk?.bytes);
238
+ if (parsed === undefined) return;
239
+ this.chunks.push(parsed);
240
+ this.text += textFromProviderChunk(parsed);
241
+ if (isRecord(parsed)) {
242
+ const delta = isRecord(parsed.delta) ? parsed.delta : undefined;
243
+ const message = isRecord(parsed.message) ? parsed.message : undefined;
244
+ const messageUsage = isRecord(message?.usage) ? message?.usage : undefined;
245
+ const usage = isRecord(parsed.usage) ? parsed.usage : undefined;
246
+ const metrics = isRecord(parsed["amazon-bedrock-invocationMetrics"])
247
+ ? parsed["amazon-bedrock-invocationMetrics"]
248
+ : undefined;
249
+ const outputs =
250
+ Array.isArray(parsed.outputs) && isRecord(parsed.outputs[0])
251
+ ? parsed.outputs[0]
252
+ : undefined;
253
+ const generation =
254
+ Array.isArray(parsed.generations) && isRecord(parsed.generations[0])
255
+ ? parsed.generations[0]
256
+ : undefined;
257
+ this.finishReason =
258
+ stringValue(delta?.stop_reason) ??
259
+ stringValue(parsed.stop_reason) ??
260
+ stringValue(outputs?.stop_reason) ??
261
+ stringValue(generation?.finish_reason) ??
262
+ stringValue(parsed.completionReason) ??
263
+ this.finishReason;
264
+ this.usage = omitUndefined({
265
+ ...this.usage,
266
+ inputTokens:
267
+ numberValue(messageUsage?.input_tokens) ??
268
+ numberValue(metrics?.inputTokenCount) ??
269
+ numberValue(parsed.inputTextTokenCount) ??
270
+ this.usage?.inputTokens,
271
+ outputTokens:
272
+ numberValue(usage?.output_tokens) ??
273
+ numberValue(metrics?.outputTokenCount) ??
274
+ numberValue(parsed.totalOutputTextTokenCount) ??
275
+ this.usage?.outputTokens,
276
+ cacheReadInputTokens:
277
+ numberValue(messageUsage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
278
+ cacheCreationInputTokens:
279
+ numberValue(messageUsage?.cache_creation_input_tokens) ??
280
+ this.usage?.cacheCreationInputTokens,
281
+ });
282
+ }
283
+ }
284
+
285
+ finish(): SpanFields {
286
+ return mergeFields(
287
+ omitUndefined({
288
+ output: this.text
289
+ ? [
290
+ {
291
+ role: "assistant",
292
+ parts: [{ type: "text", content: this.text }],
293
+ finish_reason: this.finishReason,
294
+ },
295
+ ]
296
+ : this.chunks,
297
+ finishReason: this.finishReason,
298
+ usage: this.usage,
299
+ error: this.error,
300
+ }),
301
+ this.errorFields,
302
+ );
303
+ }
304
+ }
305
+
306
+ function textFromProviderChunk(value: unknown): string {
307
+ if (!isRecord(value)) return "";
308
+ const delta = isRecord(value.delta) ? value.delta : undefined;
309
+ const contentBlockDelta = isRecord(value.contentBlockDelta)
310
+ ? value.contentBlockDelta
311
+ : isRecord(value.content_block_delta)
312
+ ? value.content_block_delta
313
+ : undefined;
314
+ const nestedDelta = isRecord(contentBlockDelta?.delta) ? contentBlockDelta?.delta : undefined;
315
+ const generation =
316
+ Array.isArray(value.generations) && isRecord(value.generations[0])
317
+ ? value.generations[0]
318
+ : undefined;
319
+ return (
320
+ stringValue(value.outputText) ??
321
+ stringValue(value.generation) ??
322
+ stringValue(value.completion) ??
323
+ stringValue(value.text) ??
324
+ stringValue(delta?.text) ??
325
+ stringValue(nestedDelta?.text) ??
326
+ (Array.isArray(value.outputs) && isRecord(value.outputs[0])
327
+ ? stringValue(value.outputs[0].text)
328
+ : undefined) ??
329
+ stringValue(generation?.text) ??
330
+ ""
331
+ );
332
+ }