Skip to content

Commit af1d5a5

Browse files
authored
fix(ai): Unexpected reasoning-start event in extract reasoning middleware (#7107)
## Background If the extract reasoning middleware is used when making a request but the model's response does not include `<think></think>` tags, the middleware will generate an unexpected reasoning-start event. ## Summary Ensure that the reasoning-start event is only emitted in real reasoning mode.
1 parent 28b92a8 commit af1d5a5

File tree

3 files changed

+95
-16
lines changed

3 files changed

+95
-16
lines changed

‎.changeset/spotty-apples-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
fix(ai): Unexpected reasoning-start event in extract reasoning middleware

‎packages/ai/core/middleware/extract-reasoning-middleware.test.ts

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -683,20 +683,12 @@ describe('extractReasoningMiddleware', () => {
683683
"id": "1",
684684
"type": "text-start",
685685
},
686-
{
687-
"id": "reasoning-0",
688-
"type": "reasoning-start",
689-
},
690686
{
691687
"id": "1",
692688
"providerMetadata": undefined,
693689
"text": "ana",
694690
"type": "text",
695691
},
696-
{
697-
"id": "reasoning-0",
698-
"type": "reasoning-start",
699-
},
700692
{
701693
"id": "1",
702694
"providerMetadata": undefined,
@@ -705,18 +697,100 @@ describe('extractReasoningMiddleware', () => {
705697
"type": "text",
706698
},
707699
{
708-
"id": "reasoning-0",
709-
"type": "reasoning-start",
700+
"id": "1",
701+
"providerMetadata": undefined,
702+
"text": "</think>",
703+
"type": "text",
710704
},
711705
{
712706
"id": "1",
713707
"providerMetadata": undefined,
714-
"text": "</think>",
708+
"text": "this is the response",
715709
"type": "text",
716710
},
717711
{
718-
"id": "reasoning-0",
719-
"type": "reasoning-start",
712+
"id": "1",
713+
"type": "text-end",
714+
},
715+
{
716+
"finishReason": "stop",
717+
"providerMetadata": undefined,
718+
"response": {
719+
"headers": undefined,
720+
"id": "id-0",
721+
"modelId": "mock-model-id",
722+
"timestamp": 1970-01-01T00:00:00.000Z,
723+
},
724+
"type": "finish-step",
725+
"usage": {
726+
"cachedInputTokens": undefined,
727+
"inputTokens": 5,
728+
"outputTokens": 10,
729+
"reasoningTokens": 3,
730+
"totalTokens": 18,
731+
},
732+
},
733+
{
734+
"finishReason": "stop",
735+
"totalUsage": {
736+
"cachedInputTokens": undefined,
737+
"inputTokens": 5,
738+
"outputTokens": 10,
739+
"reasoningTokens": 3,
740+
"totalTokens": 18,
741+
},
742+
"type": "finish",
743+
},
744+
]
745+
`);
746+
});
747+
748+
it('should keep original text when <think> tag is not present', async () => {
749+
const mockModel = new MockLanguageModelV2({
750+
async doStream() {
751+
return {
752+
stream: convertArrayToReadableStream([
753+
{
754+
type: 'response-metadata',
755+
id: 'id-0',
756+
modelId: 'mock-model-id',
757+
timestamp: new Date(0),
758+
},
759+
{ type: 'text-start', id: '1' },
760+
{ type: 'text-delta', id: '1', delta: 'this is the response' },
761+
{ type: 'text-end', id: '1' },
762+
{
763+
type: 'finish',
764+
finishReason: 'stop',
765+
usage: testUsage,
766+
},
767+
]),
768+
};
769+
},
770+
});
771+
772+
const result = streamText({
773+
model: wrapLanguageModel({
774+
model: mockModel,
775+
middleware: extractReasoningMiddleware({ tagName: 'think' }),
776+
}),
777+
prompt: 'Hello, how can I help?',
778+
});
779+
780+
expect(await convertAsyncIterableToArray(result.fullStream))
781+
.toMatchInlineSnapshot(`
782+
[
783+
{
784+
"type": "start",
785+
},
786+
{
787+
"request": {},
788+
"type": "start-step",
789+
"warnings": [],
790+
},
791+
{
792+
"id": "1",
793+
"type": "text-start",
720794
},
721795
{
722796
"id": "1",

‎packages/ai/core/middleware/extract-reasoning-middleware.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ export function extractReasoningMiddleware({
133133
: '';
134134

135135
if (
136-
(activeExtraction.afterSwitch &&
137-
activeExtraction.isReasoning) ||
138-
activeExtraction.isFirstReasoning
136+
activeExtraction.isReasoning &&
137+
(activeExtraction.afterSwitch ||
138+
activeExtraction.isFirstReasoning)
139139
) {
140140
controller.enqueue({
141141
type: 'reasoning-start',

0 commit comments

Comments
 (0)