diff --git a/.changeset/calm-squids-sparkle.md b/.changeset/calm-squids-sparkle.md new file mode 100644 index 000000000000..51ea1832f074 --- /dev/null +++ b/.changeset/calm-squids-sparkle.md @@ -0,0 +1,5 @@ +--- +'ai': patch +--- + +fix(ai): doStream should reflect transformed values diff --git a/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap b/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap index 7960b8905280..6e300109f9af 100644 --- a/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap +++ b/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap @@ -240,8 +240,8 @@ exports[`streamText > options.transform > with base transformation > telemetry s "ai.response.model": "mock-model-id", "ai.response.msToFinish": 500, "ai.response.msToFirstChunk": 100, - "ai.response.providerMetadata": "{"testProvider":{"testKey":"testValue"}}", - "ai.response.text": "Hello, world!", + "ai.response.providerMetadata": "{"testProvider":{"testKey":"TEST VALUE"}}", + "ai.response.text": "HELLO, WORLD!", "ai.response.timestamp": "1970-01-01T00:00:00.000Z", "ai.response.toolCalls": "[{"type":"tool-call","toolCallId":"call-1","toolName":"tool1","input":{"value":"VALUE"}}]", "ai.settings.maxRetries": 2, diff --git a/packages/ai/src/generate-text/stream-text.ts b/packages/ai/src/generate-text/stream-text.ts index 74ff36841eb8..eca3f0389090 100644 --- a/packages/ai/src/generate-text/stream-text.ts +++ b/packages/ai/src/generate-text/stream-text.ts @@ -1946,29 +1946,13 @@ class DefaultStreamTextResult ? JSON.stringify(stepToolCalls) : undefined; - // record telemetry information first to ensure best effort timing + // record telemetry attributes that don't depend on transforms: try { doStreamSpan.setAttributes( await selectTelemetryAttributes({ telemetry, attributes: { 'ai.response.finishReason': stepFinishReason, - 'ai.response.text': { - output: () => activeText, - }, - 'ai.response.reasoning': { - output: () => { - const reasoningParts = recordedContent.filter( - ( - c, - ): c is { type: 'reasoning'; text: string } => - c.type === 'reasoning', - ); - return reasoningParts.length > 0 - ? reasoningParts.map(r => r.text).join('\n') - : undefined; - }, - }, 'ai.response.toolCalls': { output: () => stepToolCallsJson, }, @@ -1976,8 +1960,6 @@ class DefaultStreamTextResult 'ai.response.model': stepResponse.modelId, 'ai.response.timestamp': stepResponse.timestamp.toISOString(), - 'ai.response.providerMetadata': - JSON.stringify(stepProviderMetadata), 'ai.usage.inputTokens': stepUsage.inputTokens, 'ai.usage.outputTokens': stepUsage.outputTokens, @@ -2001,9 +1983,6 @@ class DefaultStreamTextResult ); } catch (error) { // ignore error setting telemetry attributes - } finally { - // finish doStreamSpan before other operations for correct timing: - doStreamSpan.end(); } controller.enqueue({ @@ -2027,6 +2006,33 @@ class DefaultStreamTextResult // to ensure that the recorded steps are complete: await stepFinish.promise; + // set transform-dependent attributes after the step has been + // fully processed (post-transform) by the event processor: + const processedStep = + recordedSteps[recordedSteps.length - 1]; + try { + doStreamSpan.setAttributes( + await selectTelemetryAttributes({ + telemetry, + attributes: { + 'ai.response.text': { + output: () => processedStep.text, + }, + 'ai.response.reasoning': { + output: () => processedStep.reasoningText, + }, + 'ai.response.providerMetadata': JSON.stringify( + processedStep.providerMetadata, + ), + }, + }), + ); + } catch (error) { + // ignore error setting telemetry attributes + } finally { + doStreamSpan.end(); + } + const clientToolCalls = stepToolCalls.filter( toolCall => toolCall.providerExecuted !== true, );