Repository: vercel/ai (TypeScript)

Issue:
remove stray </think> tags in reasoning extractor middleware
### Description

In some cases, the </think> tag is leaking into customer-facing messages. For example:

```
Assistant: "Okay, the user is reacting to the .... \n</think>"
```

this is most likely caused by out-of-order `<think>` and `</think>` tags. the middleware should just remove them




Fix (patch):
diff --git a/.changeset/few-cherries-attack.md b/.changeset/few-cherries-attack.md
new file mode 100644
index 000000000000..8d07fa1a0549
--- /dev/null
+++ b/.changeset/few-cherries-attack.md
@@ -0,0 +1,5 @@
+---
+'ai': patch
+---
+
+fix (ai/core): add `startWithReasoning` option to `extractReasoningMiddleware`
diff --git a/content/docs/02-guides/04-r1.mdx b/content/docs/02-guides/04-r1.mdx
index 4ab6c068f8b0..0ca3ecdcf1eb 100644
--- a/content/docs/02-guides/04-r1.mdx
+++ b/content/docs/02-guides/04-r1.mdx
@@ -99,10 +99,14 @@ const { reasoning, text } = await generateText({
 });
 ```
 
-<Note>
-  The AI SDK provides a [middleware](/docs/ai-sdk-core/middleware)
-  (`extractReasoningMiddleware`) that can be used to extract the reasoning
-  tokens from the model's output.
+<Note id="deepseek-r1-middleware">
+The AI SDK provides a [middleware](/docs/ai-sdk-core/middleware)
+(`extractReasoningMiddleware`) that can be used to extract the reasoning
+tokens from the model's output.
+
+When using DeepSeek-R1 series models with third-party providers like Together AI, we recommend using the `startWithReasoning`
+option in the `extractReasoningMiddleware` function, as they tend to bypass thinking patterns.
+
 </Note>
 
 ### Model Provider Comparison
diff --git a/content/docs/03-ai-sdk-core/45-middleware.mdx b/content/docs/03-ai-sdk-core/45-middleware.mdx
index 3f3861628682..d81de55d782b 100644
--- a/content/docs/03-ai-sdk-core/45-middleware.mdx
+++ b/content/docs/03-ai-sdk-core/45-middleware.mdx
@@ -70,6 +70,11 @@ const model = wrapLanguageModel({
 
 You can then use that enhanced model in functions like `generateText` and `streamText`.
 
+The `extractReasoningMiddleware` function also includes a `startWithReasoning` option.
+When set to `true`, the reasoning tag will be prepended to the generated text.
+This is useful for models that do not include the reasoning tag at the beginning of the response.
+For more details, see the [DeepSeek R1 guide](https://sdk.vercel.ai/docs/guides/r1#deepseek-r1-middleware).
+
 ## Implementing Language Model Middleware
 
 <Note>
diff --git a/content/docs/07-reference/01-ai-sdk-core/66-extract-reasoning-middleware.mdx b/content/docs/07-reference/01-ai-sdk-core/66-extract-reasoning-middleware.mdx
index 1d9fdd5218b6..e4b4954b9365 100644
--- a/content/docs/07-reference/01-ai-sdk-core/66-extract-reasoning-middleware.mdx
+++ b/content/docs/07-reference/01-ai-sdk-core/66-extract-reasoning-middleware.mdx
@@ -43,6 +43,13 @@ const middleware = extractReasoningMiddleware({
       description:
         'The separator to use between reasoning and text sections. Defaults to "\\n"',
     },
+    {
+      name: 'startWithReasoning',
+      type: 'boolean',
+      isOptional: true,
+      description:
+        'Starts with reasoning tokens. Set to true when the response always starts with reasoning and the initial tag is omitted. Defaults to false.',
+    },
   ]}
 />
 
diff --git a/packages/ai/core/middleware/extract-reasoning-middleware.ts b/packages/ai/core/middleware/extract-reasoning-middleware.ts
index 76829577f3c6..8f3023f5a6fb 100644
--- a/packages/ai/core/middleware/extract-reasoning-middleware.ts
+++ b/packages/ai/core/middleware/extract-reasoning-middleware.ts
@@ -1,6 +1,6 @@
-import { LanguageModelV1StreamPart } from '@ai-sdk/provider';
+import type { LanguageModelV1StreamPart } from '@ai-sdk/provider';
 import { getPotentialStartIndex } from '../util/get-potential-start-index';
-import { LanguageModelV1Middleware } from './language-model-v1-middleware';
+import type { LanguageModelV1Middleware } from './language-model-v1-middleware';
 
 /**
  * Extract an XML-tagged reasoning section from the generated text and exposes it
@@ -8,13 +8,16 @@ import { LanguageModelV1Middleware } from './language-model-v1-middleware';
  *
  * @param tagName - The name of the XML tag to extract reasoning from.
  * @param separator - The separator to use between reasoning and text sections.
+ * @param startWithReasoning - Whether to start with reasoning tokens.
  */
 export function extractReasoningMiddleware({
   tagName,
   separator = '\n',
+  startWithReasoning = false,
 }: {
   tagName: string;
   separator?: string;
+  startWithReasoning?: boolean;
 }): LanguageModelV1Middleware {
   const openingTag = `<${tagName}>`;
   const closingTag = `<\/${tagName}>`;
@@ -22,12 +25,14 @@ export function extractReasoningMiddleware({
   return {
     middlewareVersion: 'v1',
     wrapGenerate: async ({ doGenerate }) => {
-      const { text, ...rest } = await doGenerate();
+      const { text: rawText, ...rest } = await doGenerate();
 
-      if (text == null) {
-        return { text, ...rest };
+      if (rawText == null) {
+        return { text: rawText, ...rest };
       }
 
+      const text = startWithReasoning ? openingTag + rawText : rawText;
+
       const regexp = new RegExp(`${openingTag}(.*?)${closingTag}`, 'gs');
       const matches = Array.from(text.matchAll(regexp));
 
@@ -61,7 +66,7 @@ export function extractReasoningMiddleware({
       let isFirstReasoning = true;
       let isFirstText = true;
       let afterSwitch = false;
-      let isReasoning: boolean = false;
+      let isReasoning = startWithReasoning;
       let buffer = '';
 
       return {